org.springframework.boot spring-boot-starter-parent 2.5.4 org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-web mysql mysql-connector-java runtime org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.security spring-security-test test com.baomidou mybatis-plus-boot-starter 3.4.2 org.slf4j slf4j-api org.thymeleaf.extras thymeleaf-extras-springsecurity5 org.springframework.boot spring-boot-starter-thymeleaf
application.yml:thymeleaf-extras-springsecurity5:thymeleaf和Spring security的整合jar包
spring:
datasource:
username: root
password: abcdefg
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springSecurityTest?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
java 目录结构配置数据库
config securityConfig.java controller UserController.java >hander MyAccessDeniedHandler.java MyAuthenticationFailureHandle.java MyAuthenticationSuccessHandle.java mapper RoleMapper.java UserMapper.java pojo Role.java Users.java service MyAccess.java MyAccessImpl.java RoleService.java UserService.java UserDetailServiceImpl.java
B>包;I>接口;C>普通类
pojo对应数据库的实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
private String uid;
private String roleName;
private String roleDesc;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Users {
private Integer id;
@TableId(type = IdType.ASSIGN_UUID)
private String uid;
private String username;
private String password;
}
mapper@TableId(type = IdType.ASSIGN_UUID):使用uid
使用mybatisPlus操作数据库
@Mapper public interface RoleMapper extends BaseMapper{ }
@Mapper public interface UsersMapper extends BaseMapperservice{ }
查询用户表的用户uid,username,paddword信息,再根据uid查询角色表对应用户角色权限
@Service
public class RoleService {
@Autowired
private RoleMapper roleMapper;
public List selectRole(String uid) {
return roleMapper.selectList(new QueryWrapper().eq("uid",uid));
}
}
@Service
public class UsersService {
@Autowired
private UsersMapper usersMapper;
public Users selectUsername(String username) {
return usersMapper.selectOne(new QueryWrapper().eq("username", username));
}
}
自定义登录认证逻辑
@Service
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UsersService usersService;
@Autowired
private RoleService roleService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//根据用户名去数据库查询,不存在就抛异常UsernameNotFoundException
Users user = usersService.selectUsername(s);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
List roles = roleService.selectRole(user.getUid());
//创建list集合用来存储用户的权限角色
ArrayList authorities = new ArrayList<>();
if (roles.size() >= 1) {
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
}
log.info("执行自定义登录逻辑");
log.info("角色权限:" + authorities);
//这个User是Security包下的User,第一个参数username,第二个参数passowrd(已加密过的),第三个参数角色权限
return new User(s, user.getPassword(), authorities);
}
}
自定义access
public interface MyAccess {
boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
@Service
public class MyAccessImpl implements MyAccess {
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
Object obj = authentication.getPrincipal();
if (obj instanceof UserDetails) {
UserDetails userDetails = (UserDetails) obj;
Collection extends GrantedAuthority> authorities = userDetails.getAuthorities();
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}
}
config
设置访问权限
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
private DataSource dataSource;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
@Autowired
private UserDetailServiceImpl userDetailService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
//登录页面
.loginPage("/toLogin")
//登录表单提交的求情
.loginProcessingUrl("/login")
//登录成功跳转页面,这个是一个请求必须是post
// .successForwardUrl("/index")
//自定义登录成功的处理器
.successHandler(new MyAuthenticationSuccessHandler("/index.html"))
//登录错误跳转页面,这个是一个请求必须是post
// .failureForwardUrl("/toError")
//自定义登录失败的处理器
.failureHandler(new MyAuthenticationFailureHandler("/error.html"))
//自定义表单的name,默认就是username和password
// .usernameParameter("username")
// .passwordParameter("password")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/toLogin")
.and()
//认证
.authorizeRequests()
//登录页面和错误页面放行不拦截
.antMatchers("/loginPage.html","/error.html","/toLogin")
.permitAll()
//权限访问,权限是随便的比如lv1,lv2等,可以设置多个权限或角色比如既是lv1又是lv2
.antMatchers("/lv1.html").hasAnyAuthority("lv1")
.antMatchers("/lv2.html").hasAnyAuthority("lv2")
.antMatchers("/lv3.html").hasAnyAuthority("lv3")
//角色访问必须是ROLE_xxx形式
.antMatchers("/lv4.html").hasRole("hzyy")
//ip地址
.antMatchers("/ip.html").hasIpAddress("127.0.0.1")
//自定义access,使用自定义的access就不能使用anyxxx的方法
// .anyRequest().access("@myAccessImpl.hasPermission(request,authentication)")
//其余所有请求
.anyRequest()
//需要认证
.authenticated()
.and()
.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
http.rememberMe()
.tokenRepository(persistentTokenRepository)
//默认就是remember-me
//.rememberMeParameter("remember-me")
.tokenValiditySeconds(60)
.userDetailsService(userDetailService);
}
@Bean
public PasswordEncoder getPW() {
return new BCryptPasswordEncoder();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//第一次需要打开会自动创建一张表
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
handler
没有权限跳转的页面也就是403页面
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpServletResponse.SC_ACCEPTED);
httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.write("{"status":"error403","msg:权限不足"}");
writer.flush();
writer.close();
}
}
自定义登录失败的逻辑,也可以使用自带的
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private String url;
public MyAuthenticationFailureHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.sendRedirect(url);
}
}
自定义登录成功的逻辑,也可以使用自带的
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String url;
public MyAuthenticationSuccessHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
User user = (User) authentication.getPrincipal();
log.info("security的user:" + user);
log.info("ip:" + httpServletRequest.getRemoteAddr());
httpServletResponse.sendRedirect(url);
}
}
Other
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true,proxyTargetClass = true,jsr250Enabled = true)
@SpringBootApplication
public class SpringSecuritySeniorApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecuritySeniorApplication.class, args);
}
}
启动类使用注解@EnableGlobalMethodSecurity开启security的注解使用,参数有四个分别对应一个注解,最好用的一个:
prePostEnabled:对应注解为@PreAuthorize,参数为表达式,可以设置权限角色
@PreAuthorize("hasAnyAuthority('lv1')") @RequestMapping("/toAnn") public String annotation() { return "redirect:annotation.html"; }
密码加密,使用的security默认的加密方式
PasswordEncoder是个接口,但在config里面已经进行了bean注入
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
@Autowired
private PasswordEncoder passwordEncoder;
@Test
void contextLoads() {
System.out.println(passwordEncoder.encode("hahha"));
}
自定义错误页面的展示
public class ErrorPageConfig implements ErrorPageRegistrar {
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
ErrorPage page404 = new ErrorPage(HttpStatus.NOT_FOUND, "/404");
ErrorPage page403 = new ErrorPage(HttpStatus.FORBIDDEN, "/403");
ErrorPage page500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500");
registry.addErrorPages(page403,page404,page500);
}
}
@Configuration
public class ErrorBean {
@Bean
public ErrorPageConfig errorPageConfig (){
return new ErrorPageConfig();
}
}
JWT pom.xmlJsonWebToken
javaio.jsonwebtoken jjwt 0.9.1
package com.xiaoyu;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.Base64Codec;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import sun.misc.BASE64Decoder;
import java.util.Date;
@SpringBootTest
class SpringSecurityJwtApplicationTests {
@Test
void creatToken() {
JwtBuilder jwtBuilder = Jwts.builder()
//唯一id
.setId("666")
//接受的用户
.setSubject("Rose")
//签发时间
.setIssuedAt(new Date())
//签名算法+密钥
.signWith(SignatureAlgorithm.HS256, "hzyyyyyyyy");
String token = jwtBuilder.compact();
System.out.println(token);
System.out.println("==========");
String[] split = token.split("\.");
System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
}
@Test
public void AnalysisToken() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjMxMjgxNTUxfQ.cWEbwr3FjzM8oyQgEEEG3-M1Im6JJ1bpSxol00WHrpY";
//Claims JWT的荷载对象
Claims claims = (Claims) Jwts.parser()
.setSigningKey("hzyyyyyyy")
.parse(token)
.getBody();
System.out.println("id" + claims.getId());
System.out.println("rose" + claims.getSubject());
System.out.println("date" + claims.getIssuedAt());
}
//设置失效时间
@Test
void creatTokenTimeout() {
long data = System.currentTimeMillis();
long exp = data+60*1000;
JwtBuilder jwtBuilder = Jwts.builder()
//唯一id
.setId("666")
//接受的用户
.setSubject("Rose")
//签发时间
.setIssuedAt(new Date())
//设置失效时间
.setExpiration(new Date(exp))
//签名算法+密钥
.signWith(SignatureAlgorithm.HS256, "hzyyyyyyyy");
String token = jwtBuilder.compact();
System.out.println(token);
System.out.println("==========");
String[] split = token.split("\.");
System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
}
@Test
public void AnalysisTokenTimeout() {
String token = "eyJhbGciOiJIUzI1NiJ9." +
"eyJqdGkiOiI2NjYiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjMxMjgyMDQ2LCJleHAiOjE2MzEyODIxMDZ9." +
"qSG3daBcCRQ92-p564daNgN7UPbquVz85c14zcj8rdQ";
//Claims JWT的荷载对象
Claims claims = (Claims) Jwts.parser()
.setSigningKey("hzyyyyyyyy")
.parse(token)
.getBody();
System.out.println("id" + claims.getId());
System.out.println("rose" + claims.getSubject());
System.out.println("date" + claims.getIssuedAt());
}
//自定义声明
@Test
void creatTokenZDY() {
JwtBuilder jwtBuilder = Jwts.builder()
//唯一id
.setId("666")
//接受的用户
.setSubject("Rose")
//签发时间
.setIssuedAt(new Date())
//添加自定义
.claim("name","zhangsan")
.claim("logo","xxx.jpg")
//签名算法+密钥
.signWith(SignatureAlgorithm.HS256, "hzyyyyyyyy");
String token = jwtBuilder.compact();
System.out.println(token);
System.out.println("==========");
String[] split = token.split("\.");
System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
}
@Test
public void AnalysisTokenZDY() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjMxMjgyMzc3LCJuYW1lIjoiemhhbmdzYW4iLCJsb2dvIjoieHh4LmpwZyJ9.QuI6RAqoZ88nHrih3xA5dcT3SaWNFYrkobhGpNR5YEw";
//Claims JWT的荷载对象
Claims claims = (Claims) Jwts.parser()
.setSigningKey("hzyyyyyyyy")
.parse(token)
.getBody();
System.out.println("id:" + claims.getId());
System.out.println("rose:" + claims.getSubject());
System.out.println("date:" + claims.getIssuedAt());
System.out.println("name:" + claims.get("name"));
System.out.println("logo:" + claims.get("logo"));
}
}
pom.xmlOAuth是一个协议有很多对应语言的实现,这使用的spring的实现
Oauth2有四种模式
1.授权码模式:例如登录页面的其他账号登录中使用微信登录
2.简化版的授权码模式
3.密码模式:使用密码登录获取token
4.客户端模式
注意Spring boot和Spring cloud的版本对应
javaorg.springframework.boot spring-boot-starter-parent 2.3.5.RELEASE 1.8 Hoxton.SR9 org.springframework.cloud spring-cloud-starter-oauth2 org.springframework.cloud spring-cloud-starter-security org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-data-redis io.jsonwebtoken jjwt 0.9.1 org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import
创建认证类继承UserDetailsService,重写loadUserByUsername()方法
@Service
public class UserService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
String password = passwordEncoder.encode("123");
return new User(s,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
创建权限类继承WebSecurityConfigurerAdapter,重写configure(HttpSecurity http)方法
bean注入PasswordEncoder,返回为BCryptPasswordEncoder():密码加密
AuthenticationManager,返回为super.authenticationManager():jwt自添加内容时要用到
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/**","/login/**").permitAll().anyRequest()
.authenticated().and().formLogin().permitAll().and()
.csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
创建jwt存储类
TokenStore:声明存储token为jwt
JwtAccessTokenConverter:添加jwt密钥
JwtTokenEnhancer:后面jwt添加自定义内容要用到
@Configuration
public class JwtTokenStoreConfig {
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("miyaonamelenght");
return jwtAccessTokenConverter;
}
@Bean
public JwtTokenEnhancer jwtTokenEnhancer() {
return new JwtTokenEnhancer();
}
}
jwt添加自定义内容类,继承TokenEnhancer重写enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication)方法
map类型可以添加多个
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map map = new HashMap<>();
map.put("uid","hzyy");
((DefaultOAuth2AccessToken)oAuth2AccessToken).setAdditionalInformation(map);
return oAuth2AccessToken;
}
}
授权服务器类,继承AuthorizationServerConfigurerAdapter
必须加注解@EnableAuthorizationServer开启授权服务
passwordEncoder:密码加密
authenticationManager:认证管理
userService:认证服务
jwtTokenStore:jwtToken存储
JwtTokenEnhancer:jwtToken增加自定义内容用
jwtAccessTokenConverter:jwtToken转换器
configure(ClientDetailsServiceConfigurer clients):配置授权服务器
configure(AuthorizationServerEndpointsConfigurer endpoints):配置jwt自定义内容
configure(AuthorizationServerSecurityConfigurer security):获取密钥要认证,单点登录需要
@Configuration
//开启授权服务器
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore jwtTokenStore;
@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
//http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirct_url=http://localhost:8080/getCode&scope=all
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//客户端id
.withClient("client")
//密钥
.secret(passwordEncoder.encode("112233"))
//重定向地址
.redirectUris("http://localhost:8080/getCode")
//授权范围
.scopes("all")
//token失效时间
.accessTokenValiditySeconds(600)
//授权类型 authorization_code:授权吗模式;password 密码模式 refresh_token刷新令牌
.authorizedGrantTypes("authorization_code","password","refresh_token")
//刷新令牌的失效时间
.refreshTokenValiditySeconds(6000)
//自动授权不需要点击确定
.autoApprove(true);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//设置增强内容
TokenEnhancerChain chain = new TokenEnhancerChain();
List delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(jwtAccessTokenConverter);
chain.setTokenEnhancers(delegates);
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userService)
.tokenStore(jwtTokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(chain);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//获取密钥必须要身份认证,单点登录必须要配置
security.tokenKeyAccess("isAuthenticated()");
}
}
资源服务器类,继承ResourceServerConfigurerAdapter,重写configure(HttpSecurity http)
需要注意的是:
- 加注解@EnableResourceServer开启资源服务
- 一定要先全部拦截然后认证进行放行
必须先.authorizeRequests().anyRequest().authenticated()再.requestMatchers().antMatchers(“/user/**”);不然会报错
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.requestMatchers()
.antMatchers("/user/**");
}
}
如果资源服务器和授权服务器分离的话需要多两个步骤
- jwtToken存储配置
@Configuration
public class JwtTokenStoreConfig {
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//设置jwt密钥
jwtAccessTokenConverter.setSigningKey("miyaonamelenght");
return jwtAccessTokenConverter;
}
}
-
添加jwt验证
和授权服务器资源服务器在一起的区别就是要添加jwtToken认证
@Autowired @Qualifier("jwtTokenStore") private TokenStore jwtToken; @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.tokenStore(jwtToken); }@Configuration @EnableResourceServer public class ResourceConfig extends ResourceServerConfigurerAdapter { @Autowired @Qualifier("jwtTokenStore") private TokenStore jwtToken; @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.tokenStore(jwtToken); } @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and() .requestMatchers() .antMatchers("/user/**"); } }



