1.
引入依赖:
1 2 3 4 5 6 7 8
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
|
一些必要的配置:
新建一个配置类 WebSecurityConfig继承WebSecurityConfigurerAdapter重写configure方法。(重要)
1 2 3 4 5 6 7 8
| @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin(); } }
|
新建一个controller用来测试登录
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
@RestController public class UserController {
@GetMapping("/user-info") public Authentication getUserInfo(Authentication authentication) { return authentication; } }
|
启动项目:终端会有这么一段日志
1
| Using generated security password: f429b724-db54-4a56-ae82-7ebb63f22d69
|
表示:没有设置用户信息,给出了一个默认用户及密码,默认用户user
登录之后,默认会跳转到Index页面,但是目前没有这个页面,所以会报错。
暂不处理。
访问:http://localhost:8080/user-info
返回结果:
1
| {"credentials":null,"details":null,"authenticated":false,"authorities":null,"principal":null,"name":"not login!"}
|
可以通过http://localhost:8080/logount退出登录
之后在访问user-info接口,发现不在包含用户信息
2.
实际使用中 没有登录的用户是不能访问接口的
修改WebSecurityConfig
1 2 3 4 5 6 7 8 9
| @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // 开启登录 http.formLogin(); // 设置访问权限 任何请求均需要认证(登录成功)才能访问 http.authorizeRequests().anyRequest().authenticated(); } }
|
重启项目
此时,访问:http://localhost:8080/user-info
发现会直接跳转到登录页面
增加一些细节
依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.11.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency></dependencies>
|
配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| server: port: 8080 spring: output: ansi: enabled: always datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: 12345678 redis: host: 127.0.0.1 port: 6379 database: 1 jwt: secretKey: a3e4cd2d191a017bf49dbdf49a4c62b1fb292c5b112d6a51bdc4e2ea5052e816 expiration: 3600 logging: pattern: console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(%-40.40logger{39}){cyan} : %msg%n" mybatis: type-aliases-package: com.li.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:mapper/*.xml
|
- 在
com.li新建utils包,新建JwtUtils
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| @Component @Slf4j public class JwtUtils { @Value("${jwt.secretKey}") private String jwtSecretKey; @Value("${jwt.expiration}") private long expiration;
public String createToken(String userInfo, List<String> authList) { Date currentTime = new Date(); Date expireTime = new Date(currentTime.getTime() + expiration); Map<String, Object> headerClaims = new HashMap<>(); headerClaims.put("type", "JWT"); headerClaims.put("alg", "HS256"); return JWT.create() .withHeader(headerClaims) .withIssuedAt(currentTime) .withExpiresAt(expireTime) .withIssuer("thomas") .withClaim("userInfo", userInfo) .withClaim("authList", authList) .sign(Algorithm.HMAC256(jwtSecretKey));
}
public boolean verifyToken(String token) { JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build(); try { DecodedJWT decodedJWT = jwtVerifier.verify(token); return true; } catch (JWTVerificationException e) { log.error("验签失败:{}", token); } return false; }
public String getUserInfo(String token) { JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build(); try { DecodedJWT decodedJWT = jwtVerifier.verify(token); return decodedJWT.getClaim("userInfo").asString(); } catch (JWTVerificationException e) { log.error("getUserInfo error", e); } return null; }
public List<String> getUserAuth(String token) { JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build(); try { DecodedJWT decodedJWT = jwtVerifier.verify(token); return decodedJWT.getClaim("authList").asList(String.class); } catch (JWTVerificationException e) { log.error("getUserAuth error", e); } return null; } }
|
- 在
com.li新建filter包,新建SaySomethingJWTFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
|
@Component public class SaySomethingJWTFilter extends OncePerRequestFilter { @Resource private ObjectMapper objectMapper; @Resource private StringRedisTemplate stringRedisTemplate; @Resource private JwtUtils jwtUtils; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String requestURI = request.getRequestURI(); if (requestURI.equals("/login")) { filterChain.doFilter(request, response); return; } String authorization = request.getHeader("Authorization"); if (!StringUtils.hasText(authorization)) { printFront(response, "没有登录!"); return; } String jwtToken = authorization.replace("Bearer ", ""); boolean verifyTokenResult = jwtUtils.verifyToken(jwtToken); if (!verifyTokenResult) { printFront(response, "jwtToken 已过期"); return; } String userInfo = jwtUtils.getUserInfo(jwtToken); List<String> userAuth = jwtUtils.getUserAuth(jwtToken); SysUser sysUser = objectMapper.readValue(userInfo, SysUser.class); SecurityUser securityUser = new SecurityUser(sysUser); List<SimpleGrantedAuthority> authList = userAuth.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); securityUser.setAuthorities(authList); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToke = new UsernamePasswordAuthenticationToken(securityUser , null, authList); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToke); filterChain.doFilter(request, response); } private void printFront(HttpServletResponse response, String message) throws IOException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=utf-8"); PrintWriter writer = response.getWriter(); HttpResult httpResult = new HttpResult(); httpResult.setCode(-1); httpResult.setMsg(message); writer.print(objectMapper.writeValueAsString(httpResult)); writer.flush(); } }
|
- 调整
SecurityConfig,将过滤器添加到配置中
1 2 3 4 5 6 7 8 9
| @Resource private SaySTokenFilter saySTokenFilter;
@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(saySomethingJWTFilter, UsernamePasswordAuthenticationFilter.class);
}
|
调试
1 2 3 4
| # 不携带token访问http://localhost:8080/user-info 返回:{"code":-1,"msg":"没有登录!","data":null} # 携带错误token访问http://localhost:8080/user-info 返回:{"code":-1,"msg":"jwtToken 已过期","data":null}
|
- 在
com.li.config,新建SaySAuthenticationSuccessHandler
设置权限
在loadUserByUsername中获取权限,并设置到SecurityUser中
1 2 3 4 5 6 7 8 9 10 11
|
SecurityUser securityUser = new SecurityUser(sysUser);
List<String> authList = sysMenuDao.queryPermissionByUserId(sysUser.getUserId()); if (!CollectionUtils.isEmpty(authList)) { List<SimpleGrantedAuthority> authorities = authList.stream().map(SimpleGrantedAuthority::new).collect(toList()); securityUser.setAuthorities(authorities); } return securityUser;
|
在SaySAuthenticationSuccessHandler.onAuthenticationSuccess中,生成Token时,可以将权限信息一起放入Token中。
1 2 3 4 5 6 7 8 9 10
| List<String> authList = new ArrayList<>();
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) securityUser.getAuthorities(); if (!CollectionUtils.isEmpty(authorities)) { authList = authorities.stream().map(SimpleGrantedAuthority::getAuthority).collect(Collectors.toList()); }
String token = saySJwtUtils.createToken(userInfo, authList);
|
注销处理
Jwt本质上是一个字符串,无法手动将其过期,也就是说,即使手动退出登录,对于Token来说,还是一个有效的Token,可以通过接入Redis来解决这一问题
- 登录成功时,将Token写入Redis
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
@Value("${jwt.expiration}") private long expiration;
@Resource private StringRedisTemplate stringRedisTemplate;
onAuthenticationSuccess(){
String token = saySJwtUtils.createToken(userInfo, authList);
stringRedisTemplate.opsForValue().set("login_token:" + token, objectMapper.writeValueAsString(authentication), expiration, TimeUnit.MILLISECONDS); }
|
- 校验Token时,先验签,再去Redis中判断Token是否还存在
- 如果验签成功,但是Redis中不存在,说明Token被手动过期了
1 2 3 4 5 6 7 8 9 10
| doFilterInternal(){ ...
String tokenInRedis = stringRedisTemplate.opsForValue().get("login_token:" + jwtToken); if (!StringUtils.hasText(tokenInRedis)) { printFront(response, "用户已退出,请重新登录"); return; } ... }
|
在com.li.config,新建SaysLogoutSuccessHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
@Component public class SaysLogoutSuccessHandler implements LogoutSuccessHandler { @Resource private ObjectMapper objectMapper; @Resource private StringRedisTemplate stringRedisTemplate; @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { String authorization = request.getHeader("Authorization"); if (null == authorization) { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=utf-8"); HttpResult httpResult = HttpResult.builder().code(-1).msg("token不能为空").build(); PrintWriter writer = response.getWriter(); writer.write(objectMapper.writeValueAsString(httpResult)); writer.flush(); return; } String token = authorization.replace("Bearer ", ""); stringRedisTemplate.delete("login_token:" + token); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=utf-8"); HttpResult httpResult = HttpResult.builder().code(200).msg("退出成功").build(); PrintWriter writer = response.getWriter(); writer.write(objectMapper.writeValueAsString(httpResult)); writer.flush(); } }
|
调整SecurityConfig
1 2 3 4 5 6 7 8 9
| @Resource private SaysLogoutSuccessHandler saysLogoutSuccessHandler;
configure(){ http.logout().logoutSuccessHandler(saysLogoutSuccessHandler);
http.csrf().disable(); }
|
org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate
org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser
org.springframework.security.authentication.dao.DaoAuthenticationProvider#additionalAuthenticationChecks