目录
一、引入依赖及原理简述
二、多用户维护配置
三、基础自定义配置
四、前后端分离及登录结果管理
五、角色权限管理基础
从此处开始,为新的原创内容,相关数据结构代码换了一套新的,与之前的代码关系不大了。
建议新建一个项目,将配置文件复制过来,然后按照步骤走。
六、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
:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Data public class LoginDTO { private String account; private String password; }
@Data @AllArgsConstructor public class LoginVO { private Integer id; private String username; private String token; }
|
- 引入
jwt
相关依赖:
1 2 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
的相关信息:
1 2 3 4
| jwt: key: jwt-key ttl: 86400000 header: Authorization
|
- 编写对应的
Properties
类,导入配置信息:
1 2 3 4 5 6 7 8
| @Data @Component @ConfigurationProperties(prefix = "jwt") public class JWTProperties { private String key; private long ttl; private String header; }
|
- 准备
jwt
工具类:
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
|
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
自定义登录登出接口:
1 2 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
,我们新的登录登出逻辑将在此实现:
1 2 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
,主要部分为重写基于数据库的身份验证实现和废弃掉旧的验证方式
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
|
@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
1 2 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]
需要注意的是,为确保权限功能正常运行,将用户信息存入内存时,仍需要获取权限。
请读者按实际需求配置。
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
|
@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拦截器:
1 2
| http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
|
- 重写 Security 结果处理类:
[!TIP]
因为弃用了表单登录等一系列功能,结果处理类的部分改造也得以删除。
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
|
@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); } }
|
至此,自定义响应式登录配置完毕。