发布于2023-06-19 22:22 阅读(1609) 评论(0) 点赞(1) 收藏(3)
最近阿里云的项目迁回本地运行,数据库从阿里云的RDS(即Mysql5.6)换成了本地8.0,Redis也从古董级别的2.x换成了现在6,忍不住,手痒,把jdk升级到了17,用zgc垃圾回收器,源代码重新编译重新发布,结果碰到了古董的SpringBoot不支持jdk17,所以有了这篇日志。记录一下SpringBoot2+SpringSecurity+JWT升级成SpringBoot3+SpringSecurity+JWT,就像文章标题所说的,SpringSecurity已经废弃了继承WebSecurityConfigurerAdapter的配置方式,那就的从头来咯。
在Spring Security 5.7.0-M2中,Spring就废弃了WebSecurityConfigurerAdapter,因为Spring官方鼓励用户转向基于组件的安全配置。本文整理了一下新的配置方法。
在下面的例子中,我们使用Spring Security lambda DSL和HttpSecurity#authorizeHttpRequests方法来定义我们的授权规则,从而遵循最佳实践。
- @Configuration
- @EnableWebSecurity
- public class SecurityConfig {
-
- }
以下的配置在SecurityConfig内完成。
在HttpSecurity中注意使用了lambda写法,使用这种写法之后,每个设置都直接返回HttpSecurity对象,避免了多余的and()操作符。每一步具体的含义,请参考代码上的注释。
- @Bean
- public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
- http
- // 禁用basic明文验证
- .httpBasic().disable()
- // 前后端分离架构不需要csrf保护
- .csrf().disable()
- // 禁用默认登录页
- .formLogin().disable()
- // 禁用默认登出页
- .logout().disable()
- // 设置异常的EntryPoint,如果不设置,默认使用Http403ForbiddenEntryPoint
- .exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(invalidAuthenticationEntryPoint))
- // 前后端分离是无状态的,不需要session了,直接禁用。
- .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
- .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
- // 允许所有OPTIONS请求
- .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
- // 允许直接访问授权登录接口
- .requestMatchers(HttpMethod.POST, "/web/authenticate").permitAll()
- // 允许 SpringMVC 的默认错误地址匿名访问
- .requestMatchers("/error").permitAll()
- // 其他所有接口必须有Authority信息,Authority在登录成功后的UserDetailsImpl对象中默认设置“ROLE_USER”
- //.requestMatchers("/**").hasAnyAuthority("ROLE_USER")
- // 允许任意请求被已登录用户访问,不检查Authority
- .anyRequest().authenticated())
- .authenticationProvider(authenticationProvider())
- // 加我们自定义的过滤器,替代UsernamePasswordAuthenticationFilter
- .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
-
- return http.build();
- }

- @Autowired
- private UserDetailsService userDetailsService;
-
- @Bean
- public UserDetailsService userDetailsService() {
- // 调用 JwtUserDetailService实例执行实际校验
- return username -> userDetailsService.loadUserByUsername(username);
- }
这里关联到一个自定义的子类UserDetailsService,代码逻辑如下,注意需要根据实际情况改造数据库查询逻辑:
- @Component
- public class SecurityUserDetailsService implements UserDetailsService {
-
- @Autowired
- private SqlSession sqlSession;
-
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- try {
- // 查询数据库用户表,获得用户信息
- sqlSession.xxx
- // 使用获得的信息创建SecurityUserDetails
- SecurityUserDetails user = new SecurityUserDetails(username,
- password,
- // 以及其他org.springframework.security.core.userdetails.UserDetails接口要求的信息
- );
-
- logger.info("用户信息:{}", user);
- return user;
- } catch (Exception e) {
- String msg = "Username: " + username + " not found";
- logger.error(msg, e);
- throw new UsernameNotFoundException(msg);
- }
- }
- }

- @Bean
- public PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
这里设定使用DaoAuthenticationProvider执行具体的校验检查,并且将自定义的用户登录查询服务Bean,和密码生成器都注入到该对象中
- /**
- * 调用loadUserByUsername获得UserDetail信息,在AbstractUserDetailsAuthenticationProvider里执行用户状态检查
- *
- * @return
- */
- @Bean
- public AuthenticationProvider authenticationProvider() {
- DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
- // DaoAuthenticationProvider 从自定义的 userDetailsService.loadUserByUsername 方法获取UserDetails
- authProvider.setUserDetailsService(userDetailsService());
- // 设置密码编辑器
- authProvider.setPasswordEncoder(passwordEncoder());
- return authProvider;
- }
- /**
- * 登录时需要调用AuthenticationManager.authenticate执行一次校验
- *
- * @param config
- * @return
- * @throws Exception
- */
- @Bean
- public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
- return config.getAuthenticationManager();
- }
在拦截器中检查jwt是否能够通过签名验证,是否还在有效期内。如果通过验证,使用jwt中的信息生成一个不含密码信息的SecurityUserDetails对象,并设置到SecurityContext中,确保后续的过滤器检查能够知晓本次请求是被授权过的。具体代码逻辑看3.2. jwt请求过滤器。
- @Bean
- public JwtTokenOncePerRequestFilter authenticationJwtTokenFilter() {
- return new JwtTokenOncePerRequestFilter();
- }
- @Configuration
- @EnableWebSecurity
- public class SecurityConfig {
-
- @Autowired
- private UserDetailsService userDetailsService;
-
- @Autowired
- private InvalidAuthenticationEntryPoint invalidAuthenticationEntryPoint;
-
- @Bean
- public PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
-
- @Bean
- public JwtTokenOncePerRequestFilter authenticationJwtTokenFilter() {
- return new JwtTokenOncePerRequestFilter();
- }
-
- @Bean
- public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
- http
- // 禁用basic明文验证
- .httpBasic().disable()
- // 前后端分离架构不需要csrf保护
- .csrf().disable()
- // 禁用默认登录页
- .formLogin().disable()
- // 禁用默认登出页
- .logout().disable()
- // 设置异常的EntryPoint,如果不设置,默认使用Http403ForbiddenEntryPoint
- .exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(invalidAuthenticationEntryPoint))
- // 前后端分离是无状态的,不需要session了,直接禁用。
- .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
- .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
- // 允许所有OPTIONS请求
- .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
- // 允许直接访问授权登录接口
- .requestMatchers(HttpMethod.POST, "/web/authenticate").permitAll()
- // 允许 SpringMVC 的默认错误地址匿名访问
- .requestMatchers("/error").permitAll()
- // 其他所有接口必须有Authority信息,Authority在登录成功后的UserDetailsImpl对象中默认设置“ROLE_USER”
- //.requestMatchers("/**").hasAnyAuthority("ROLE_USER")
- // 允许任意请求被已登录用户访问,不检查Authority
- .anyRequest().authenticated())
- .authenticationProvider(authenticationProvider())
- // 加我们自定义的过滤器,替代UsernamePasswordAuthenticationFilter
- .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
-
- return http.build();
- }
-
- @Bean
- public UserDetailsService userDetailsService() {
- // 调用 JwtUserDetailService实例执行实际校验
- return username -> userDetailsService.loadUserByUsername(username);
- }
-
- /**
- * 调用loadUserByUsername获得UserDetail信息,在AbstractUserDetailsAuthenticationProvider里执行用户状态检查
- *
- * @return
- */
- @Bean
- public AuthenticationProvider authenticationProvider() {
- DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
- // DaoAuthenticationProvider 从自定义的 userDetailsService.loadUserByUsername 方法获取UserDetails
- authProvider.setUserDetailsService(userDetailsService());
- // 设置密码编辑器
- authProvider.setPasswordEncoder(passwordEncoder());
- return authProvider;
- }
-
- /**
- * 登录时需要调用AuthenticationManager.authenticate执行一次校验
- *
- * @param config
- * @return
- * @throws Exception
- */
- @Bean
- public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
- return config.getAuthenticationManager();
- }
- }

- /**
- * 每次请求的 Security 过滤类。执行jwt有效性检查,如果失败,不会设置 SecurityContextHolder 信息,会进入 AuthenticationEntryPoint
- */
- public class JwtTokenOncePerRequestFilter extends OncePerRequestFilter {
-
- @Autowired
- private JwtTokenProvider jwtTokenProvider;
-
- @Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
- throws ServletException, IOException {
- try {
- String token = jwtTokenProvider.resolveToken(request);
- if (token != null && jwtTokenProvider.validateToken(token)) {
- Authentication auth = jwtTokenProvider.getAuthentication(token);
-
- if (auth != null) {
- SecurityContextHolder.getContext().setAuthentication(auth);
- }
- }
- } catch (Exception e) {
- logger.error("Cannot set user authentication!", e);
- }
-
- filterChain.doFilter(request, response);
- }
-
- }

考虑到jwt签名验签的可靠性,以及jwt的有效载荷并未被加密,所以jwt中放置了UserDetails接口除密码字段外其他所有字段以及项目所需的业务字段。两个目的,1. 浏览器端可以解码jwt的有效载荷部分的内容用于业务处理,由于不涉及到敏感信息,不担心泄密;2.服务端可以通过jwt的信息重新生成UserDetails对象,并设置到SecurityContext中,用于请求拦截的授权校验。
代码如下:
- @RestController
- @RequestMapping("/web")
- public class AuthController {
-
- @PostMapping(value="/authenticate")
- public ResponseEntity<?> authenticate(@RequestBody Map<String, String> param) {
- logger.debug("登录请求参数:{}", param);
-
- try {
- String username = param.get("username");
- String password = param.get("password");
- String reqType = param.get("type");
-
- // 传递用户密码给到SpringSecurity执行校验,如果校验失败,会进入BadCredentialsException
- Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
- // 验证通过,设置授权信息至SecurityContextHolder
- SecurityContextHolder.getContext().setAuthentication(authentication);
-
- // 如果验证通过了,从返回的authentication里获得完整的UserDetails信息
- SecurityUserDetails userDetails = (SecurityUserDetails) authentication.getPrincipal();
-
- // 将用户的ID、名称等信息保存在jwt的token中
- String token = jwtTokenProvider.createToken(userDetails.getUsername(), userDetails.getCustname(), new ArrayList<>());
-
- // 设置cookie,本项目中非必需,因以要求必须传head的Authorization: Bearer 参数
- ResponseCookie jwtCookie = jwtTokenProvider.generateJwtCookie(token);
- Map<String, Object> model = new HashMap<>();
- model.put("username", username);
- model.put("token", token);
- return ok().body(RespBody.build().ok("登录成功", model));
- } catch (BadCredentialsException e) {
- return ok(RespBody.build().fail("账号或密码错误!"));
- }
- }
- }

经过以上步骤,对SpringSecurity6结合jwt机制进行校验的过程就全部完成了,jwt的工具类可以按照项目自己的情况进行编码。近期会修改我位于github的示例工程,提供完整的SpringBoot3+SpringSecurity6+jwt的示例工程。目前有一个SpringBoot2的老版本在这里。jwt的工具类也可以参考老版本的这个。
原文链接:https://blog.csdn.net/xieshaohu/article/details/129780439
作者:我是小豆丁
链接:http://www.javaheidong.com/blog/article/674483/9e8e07a83dcb6b1e86c3/
来源:java黑洞网
任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任
昵称:
评论内容:(最多支持255个字符)
---无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事,而不是让内心的烦躁、焦虑,坏掉你本来就不多的热情和定力
Copyright © 2018-2021 java黑洞网 All Rights Reserved 版权所有,并保留所有权利。京ICP备18063182号-2
投诉与举报,广告合作请联系vgs_info@163.com或QQ3083709327
免责声明:网站文章均由用户上传,仅供读者学习交流使用,禁止用做商业用途。若文章涉及色情,反动,侵权等违法信息,请向我们举报,一经核实我们会立即删除!