目录
一、引入依赖及原理简述
二、多用户维护配置
三、基础自定义配置
四、前后端分离及登录结果管理
五、角色权限管理基础
从此处开始,为新的原创内容,相关数据结构代码换了一套新的,与之前的代码关系不大了。
建议新建一个项目,将配置文件复制过来,然后按照步骤走。
六、RBAC 结构实现
七、自定义响应式登录与 JWT 配置 <— 你在这里 ( •̀ ω •́ )y
八、集成 Redis
Spring Security(七)自定义响应式登录与 JWT 配置
博主前言:本以为这个就是代替传统 jwt 的插件,没想到复杂程度如此之高。Spring Security 本身是个高度自定义化的组件,必须花时间重点学习一下。以下为个人配置学习的流程,从零到权限管理、redis嵌入等步骤。
本文基于尚硅谷的 Spring Security 教程学习,文章与原教程有不小出入,仅供参考。
B站视频链接:尚硅谷Java项目SpringSecurity+OAuth2权限管理实战教程
Spring Security 自带的登录接口是基于表单形式的,而对于前后端分离项目,更多运用响应式的json形式。若想改为json,或者做更复杂的修改(例如双端双接口登录等),就需要自定义登录接口了。
[!WARNING]
既然用到了json传递数据,这里就不再赘述【序列化与反序列化】的问题。
一、前置准备工作
[!IMPORTANT]
数据结构请参考上一篇文章:六、RBAC 结构实现
- 登录操作对应的DTO和VO:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | @Datapublic class LoginDTO {
 private String account;
 private String password;
 }
 
 @Data
 @AllArgsConstructor
 public class LoginVO {
 private Integer id;
 private String username;
 private String token;
 }
 
 | 
- 引入jwt相关依赖:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | <dependency>
 <groupId>io.jsonwebtoken</groupId>
 <artifactId>jjwt</artifactId>
 <version>0.9.1</version>
 </dependency>
 <dependency>
 <groupId>javax.xml.bind</groupId>
 <artifactId>jaxb-api</artifactId>
 <version>2.3.1</version>
 </dependency>
 
 | 
- 配置文件编写jwt的相关信息:
| 12
 3
 4
 
 | jwt:key: jwt-key
 ttl: 86400000
 header: Authorization
 
 | 
- 编写对应的Properties类,导入配置信息:
| 12
 3
 4
 5
 6
 7
 8
 
 | @Data@Component
 @ConfigurationProperties(prefix = "jwt")
 public class JWTProperties {
 private String key;
 private long ttl;
 private String header;
 }
 
 | 
- 准备jwt工具类:
| 12
 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
 
 | 
 
 
 
 public class JWTUtils {
 
 
 
 
 private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
 
 
 
 
 
 
 
 
 
 public static String createJWT(Map<String, Object> claims, String key, Long ttl) {
 return Jwts.builder()
 
 .signWith(SIGNATURE_ALGORITHM, key)
 
 .addClaims(claims)
 
 .setExpiration(new Date(System.currentTimeMillis() + ttl))
 .compact();
 }
 
 
 
 
 
 
 
 
 public static Claims parseJWT(String token, String key) {
 return Jwts.parser()
 .setSigningKey(key)
 .parseClaimsJws(token)
 .getBody();
 }
 }
 
 | 
- Controller自定义登录登出接口:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | @RestController@RequestMapping
 @RequiredArgsConstructor
 public class LoginController {
 private final LoginService loginService;
 
 @PostMapping("/login")
 public Result<LoginVO> login(@RequestBody LoginDTO dto) {
 return Result.success(loginService.login(dto.getAccount(), dto.getPassword()));
 }
 }
 
 | 
- 准备LoginService,我们新的登录登出逻辑将在此实现:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | public interface LoginService {LoginVO login(String account, String password);
 }
 
 @Service
 @RequiredArgsConstructor
 public class LoginServiceImpl implements LoginService {
 
 @Override
 public LoginVO login(String account, String password) {
 
 return null;
 }
 }
 
 | 
二、重写 Spring Security 的登录实现
- 修改Config,主要部分为重写基于数据库的身份验证实现和废弃掉旧的验证方式
| 12
 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
 
 | 
 
 
 
 @Configuration
 @EnableMethodSecurity
 @RequiredArgsConstructor
 public class SecurityConfig {
 private final UserService userService;
 private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
 private final SecurityResultHandler securityResultHandler;
 
 
 
 
 
 
 @Bean
 public PasswordEncoder passwordEncoder() {
 return new BCryptPasswordEncoder();
 }
 
 
 
 
 @Bean
 public AuthenticationManager authenticationManager() {
 var daoAuthenticationProvider = new DaoAuthenticationProvider();
 
 daoAuthenticationProvider.setUserDetailsService(userService);
 
 daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
 return new ProviderManager(daoAuthenticationProvider);
 }
 
 
 
 
 
 
 
 @Bean
 public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
 return http
 
 .csrf(AbstractHttpConfigurer::disable)
 
 .cors(Customizer.withDefaults())
 
 .authorizeHttpRequests(authorize -> authorize
 
 .requestMatchers("/admin/**").hasRole("ADMIN")
 .requestMatchers("/user/**").hasRole("USER")
 
 .requestMatchers("/login").permitAll()
 
 .anyRequest().authenticated())
 
 .formLogin(AbstractHttpConfigurer::disable)
 
 .httpBasic(AbstractHttpConfigurer::disable)
 
 .exceptionHandling(exception -> exception
 
 .authenticationEntryPoint(securityResultHandler)
 
 .accessDeniedHandler(securityResultHandler)
 )
 .build();
 }
 }
 
 | 
- 实现新的登录逻辑,并生成jwt
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 
 | private final JWTProperties jwtProperties;private final AuthenticationManager authenticationManager;
 
 @Override
 public LoginVO login(String account, String password) {
 
 var authenticationToken = new UsernamePasswordAuthenticationToken(account, password);
 Authentication authentication = authenticationManager.authenticate(authenticationToken);
 SecurityContextHolder.getContext().setAuthentication(authentication);
 
 
 var user = (User) authentication.getPrincipal();
 var claims = new HashMap<String, Object>();
 claims.put("id", user.getId());
 claims.put("username", user.getUsername());
 var token = JWTUtils.createJWT(claims, jwtProperties.getKey(), jwtProperties.getTtl());
 
 
 return new LoginVO(user.getId(), user.getUsername(), token);
 }
 
 | 
三、添加jwt校验拦截
- 创建jwt拦截器:
[!CAUTION]
需要注意的是,为确保权限功能正常运行,将用户信息存入内存时,仍需要获取权限。
请读者按实际需求配置。
| 12
 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
 
 | 
 
 
 
 @Slf4j
 @Component
 @RequiredArgsConstructor
 public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
 private final JWTProperties jwtProperties;
 private final RoleService roleService;
 private final PermissionService permissionService;
 
 @Override
 protected void doFilterInternal(
 @NonNull HttpServletRequest request,
 @NonNull HttpServletResponse response,
 @NonNull FilterChain filterChain)
 throws ServletException, IOException {
 
 if (request.getMethod().equals("POST")
 && request.getRequestURI().equals("/login")) {
 filterChain.doFilter(request, response);
 return;
 }
 
 
 String token = request.getHeader(jwtProperties.getHeader());
 if (token == null) {
 log.warn("请求头未传递 token");
 throw new IllegalArgumentException("请求头未传递 token");
 }
 Claims claims;
 try {
 claims = JWTUtils.parseJWT(token, jwtProperties.getKey());
 } catch (MalformedJwtException e) {
 log.warn("token 签名无效");
 throw e;
 } catch (SignatureException e) {
 log.warn("token 签名错误");
 throw e;
 } catch (UnsupportedJwtException e) {
 log.warn("token 算法不一致");
 throw e;
 } catch (ExpiredJwtException e) {
 log.warn("token 过期");
 throw e;
 } catch (Exception e) {
 log.warn("未知 token 拦截错误 {}", e.getMessage());
 throw e;
 }
 
 var userId = ((Number) claims.get("id")).longValue();
 var username = (String) claims.get("username");
 
 
 var authorityList = new ArrayList<GrantedAuthority>();
 List<Role> roleList = roleService.getByUserId(userId);
 roleList.forEach(role -> {
 
 authorityList.add(() -> "ROLE_" + role.getType());
 
 
 List<Permission> permissionList = permissionService.getByRoleId(role.getId());
 permissionList.forEach(permission -> authorityList.add(permission::getValue));
 });
 
 
 var authenticationToken = new UsernamePasswordAuthenticationToken(username, null, authorityList);
 SecurityContextHolder.getContext().setAuthentication(authenticationToken);
 filterChain.doFilter(request, response);
 }
 }
 
 | 
- 在Config类中注册jwt拦截器:
| 12
 
 | http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
 
 | 
- 重写 Security 结果处理类:
[!TIP]
因为弃用了表单登录等一系列功能,结果处理类的部分改造也得以删除。
| 12
 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
 
 | 
 
 
 
 @Slf4j
 @Component
 public class SecurityResultHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
 
 
 
 
 
 
 
 
 @Override
 public void commence(
 HttpServletRequest request,
 HttpServletResponse response,
 AuthenticationException authException)
 throws IOException {
 String resMsg = "登录已过期,请重新登录";
 if (authException instanceof BadCredentialsException)
 resMsg = authException.getMessage();
 
 
 var result = Result.error(-1, resMsg);
 var resultJSON = JSON.toJSONString(result);
 
 
 response.setContentType("application/json;charset=UTF-8");
 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
 response.getWriter().println(resultJSON);
 }
 
 
 
 
 
 
 
 
 @Override
 public void handle(
 HttpServletRequest request,
 HttpServletResponse response,
 AccessDeniedException accessDeniedException)
 throws IOException {
 log.warn("用户尝试越权操作:{}", accessDeniedException.getMessage());
 
 
 var result = Result.error(-1, "该用户无权访问");
 var resultJSON = JSON.toJSONString(result);
 
 
 response.setContentType("application/json;charset=UTF-8");
 response.setStatus(HttpServletResponse.SC_FORBIDDEN);
 response.getWriter().println(resultJSON);
 }
 }
 
 | 
至此,自定义响应式登录配置完毕。