Spring Security实战

一、登录认证方式(两种)
  1. 使用HttpSecurity的表单提交方式:loginProcessingUrl定义登录接口,不需要自己再编写登录接口,SpringSecurity自动验证;(本节介绍此种)
  2. 自己在Controller中编写登录接口,自己进行验证;
二、认证流程

​ 在 Spring Security 中,用户名和密码的验证实际上是由 UsernamePasswordAuthenticationFilter 这个过滤器类负责完成的。

1. UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter 过滤器是 Spring Security 中负责处理用户登录的主要组件。它会在用户提交包含用户名和密码的登录表单时拦截请求,并通过指定的 AuthenticationManager 进行认证,在 AuthenticationManager 内部的实现类中完成。

2. attemptAuthentication方法处理
  • UsernamePasswordAuthenticationFilter 通过配置的 loginProcessingUrl(登录接口)拦截 POST 请求。
  • 从请求中提取用户名和密码(默认从 usernamepassword 参数中获取,可自定义)。
  • 将这些凭证传递给配置的 AuthenticationManager 进行认证。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    // 处理认证的方法
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        // 将用户输入的用户名和密码封装成一个 UsernamePasswordAuthenticationToken 对象
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        // 通过 AuthenticationManager 进行认证
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}
3. AuthenticationManager

UsernamePasswordAuthenticationFilter 将用户输入的用户名和密码封装成一个 UsernamePasswordAuthenticationToken 对象,然后交给 AuthenticationManager 进行验证。

​ 在 Spring Security 默认的配置中,AuthenticationManager 是一个 ProviderManager,而 ProviderManager 会委托给一个或多个 AuthenticationProvider 来处理具体的认证过程。对于用户名和密码认证,Spring Security 默认使用的是 DaoAuthenticationProvider

4. DaoAuthenticationProvider

DaoAuthenticationProvider 是 Spring Security 默认使用的认证提供者,它负责验证用户名和密码。其工作流程如下:

  1. 查询用户DaoAuthenticationProvider 会使用一个 UserDetailsService (需重写)实现来加载用户信息,即查询数据库中的用户数据。
  2. 验证密码DaoAuthenticationProvider 会使用 PasswordEncoder(如 BCryptPasswordEncoder)来校验输入的密码是否和数据库中的密码匹配。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    @Override
    protected Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        
        // 使用 UserDetailsService 加载用户信息
        UserDetails userDetails = this.getUserDetailsService().loadUserByUsername(username);
        
        // 使用 PasswordEncoder 验证密码
        if (!passwordEncoder.matches(password, userDetails.getPassword())) {
            throw new BadCredentialsException("Bad credentials");
        }

        return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
    }
}
三、其他关键概念
1.SecurityContext

​ 安全上下文,用户通过Spring Security 的校验之后,验证信息存储在SecurityContext中。

2.SecurityContextHolder

​ 维持SecurityContext的实例,其将上下文存储为HTTP请求之间的HttpSession属性。它会为每个请求恢复上下文SecurityContextHolder,并且最重要的是,在请求完成时清除SecurityContextHolderSecurityContextHolder是一个类,他的功能方法都是静态的。

3.Authentication

Authentication是一个接口,表示登录用户是谁。

4.UserDetails

​ Spring Security自带用户信息接口,需要实现其中方法。

四、常用SpringSecurity配置类

​ Spring Security配置类实现权限认证的流程如下:

1. 表单登录配置
http.formLogin()
    .loginProcessingUrl("/users/login")
    .successHandler(authenticationSuccessHandler)
    .failureHandler(authenticationFailureHandler);
  • http.formLogin():启用表单登录(formLogin)功能,表示通过表单提交进行认证。

  • .loginProcessingUrl("/users/login"):指定登录表单的提交 URL,当用户提交登录表单时,表单中的用户名和密码会通过 POST 请求发送到这个 URL,UsernamePasswordAuthenticationFilter 会拦截这个请求,进而触发身份认证处理。

    (1)提取表单中的用户名和密码UsernamePasswordAuthenticationFilter 会从登录表单的 POST 请求中提取用户名和密码。默认情况下,它会从请求的 usernamepassword 参数中获取这两个值。

    (2)身份验证:Spring Security 使用一个认证管理器(AuthenticationManager)来验证提供的用户名和密码。默认情况下,UsernamePasswordAuthenticationFilter 会调用 AuthenticationManagerauthenticate() 方法,通常会使用一个 DaoAuthenticationProvider 来验证用户名和密码:

    • DaoAuthenticationProvider 会查询数据库,检查用户名是否存在,如果存在,则验证密码是否正确;

      1)查询用户DaoAuthenticationProvider 会使用一个 UserDetailsService 实现来加载用户信息,通常会查询数据库中的用户数据。

      2)验证密码DaoAuthenticationProvider 会使用 PasswordEncoder(如 BCryptPasswordEncoder)来校验输入的密码是否和数据库中的密码匹配。

    • 如果用户名和密码都正确,Spring Security 会创建一个 Authentication 对象(通常是 UsernamePasswordAuthenticationToken)表示认证成功。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    @Override
    protected Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        
        // 使用 UserDetailsService 加载用户信息
        UserDetails userDetails = this.getUserDetailsService().loadUserByUsername(username);
        
        // 使用 PasswordEncoder 验证密码
        if (!passwordEncoder.matches(password, userDetails.getPassword())) {
            throw new BadCredentialsException("Bad credentials");
        }

        return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
    }
}
  • .successHandler(authenticationSuccessHandler):设置认证成功后的处理器(authenticationSuccessHandler)。通常自定义处理逻辑,比如重定向到用户首页,或者返回成功响应和token。
  • .failureHandler(authenticationFailureHandler):设置认证失败后的处理器(authenticationFailureHandler)。可以根据失败的原因返回不同的错误信息或者重定向到失败页面。
2. 授权请求配置
http.authorizeRequests()
    .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
        @Override
        public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
            fsi.setSecurityMetadataSource(securityMetadataSource());
            fsi.setAccessDecisionManager(accessDecisionManager());
            return fsi;
        }
    })
    .anyRequest().permitAll()
    .and()
  • http.authorizeRequests():配置 HTTP 请求的授权规则。
  • .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {...}):对 FilterSecurityInterceptor 进行后处理。FilterSecurityInterceptor 是 Spring Security 中用于访问决策的核心组件。它根据请求和用户的权限,决定是否允许访问该资源。
    • setSecurityMetadataSource(securityMetadataSource()):设置安全元数据源(securityMetadataSource),它描述了哪些 URL 需要哪些权限。
    • setAccessDecisionManager(accessDecisionManager()):设置访问决策管理器(accessDecisionManager),它根据安全元数据源中的信息判断当前用户是否有权限访问某个资源。
  • .anyRequest().permitAll():配置对所有请求的默认授权规则,这里指定了允许所有请求不需要权限认证即可访问。
3. CSRF 关闭和异常处理
.csrf().disable().exceptionHandling()
    .authenticationEntryPoint(authenticationEntryPoint)
    .accessDeniedHandler(accessDeniedHandler)
    .and()
  • .csrf().disable():禁用 CSRF(跨站请求伪造)保护。通常,CSRF 防护在状态管理(Session)应用中启用,但是如果你使用 JWT 或其他无状态的认证方式时,可以禁用 CSRF。
  • .exceptionHandling():配置异常处理。
    • .authenticationEntryPoint(authenticationEntryPoint):配置认证入口点(authenticationEntryPoint),如果请求未认证且需要认证时,会进入该入口点。通常是返回 401(未授权)错误。
    • .accessDeniedHandler(accessDeniedHandler):配置访问被拒绝时的处理器(accessDeniedHandler)。例如,用户访问没有权限的资源时,返回 403(禁止访问)错误。
4. 无状态的会话管理
.sessionManagement()
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  • .sessionManagement():配置会话管理。
  • .sessionCreationPolicy(SessionCreationPolicy.STATELESS):指定会话管理策略为无状态(STATELESS)。这意味着服务器不会创建和维护 HTTP 会话,所有的认证信息都通过请求本身(例如 JWT 令牌)进行传递,而不依赖于会话(Session)。
5. 添加自定义 JWT 过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

​ 将自定义的 jwtAuthenticationTokenFilter 过滤器插入到 UsernamePasswordAuthenticationFilter 之前, 用来拦截请求,提取 JWT 令牌并验证其合法性。如果令牌有效,它将设置认证信息到 SecurityContext 中,这样后续的请求可以访问到认证信息。

6. 总结

通过配置 Spring Security 实现了以下功能,结合 JWT 认证和无状态会话管理,实现基于令牌的认证方式。

  1. 表单登录:通过 POST 请求到 /users/login 进行用户认证,认证成功或失败后有自定义的处理器。
  2. 权限管理
    • 配置了权限控制,通过 FilterSecurityInterceptor 来决定哪些资源需要认证,哪些可以开放。
    • 默认所有请求都允许访问(permitAll()),这通常用于公开 API,或者作为开发阶段的配置。
  3. 异常处理:对认证失败或权限不足的请求提供定制化的错误处理。
  4. 无状态会话管理:使用 SessionCreationPolicy.STATELESS,让系统无状态运行,适用于 JWT 或其他令牌机制。
  5. JWT 过滤器:在请求处理链中添加了自定义的 JWT 认证过滤器,用于从请求中提取并验证 JWT。
五、实战代码
1、引入Maven依赖
<!--   SpringSecurity安全模块	-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、创建SecurityConfig配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Resource
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Resource
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    
//    // 自定义验证过程,如不重写则使用默认的
//    @Override
//    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        // 配置自己的用户认证逻辑,例如使用用户名和密码进行认证
//    }

    // 处理认证(Authentication)请求的接口
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    // 密码加密,添加此bean则开启密码加解密
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()	// 关闭csrf防护
                .formLogin()	// 表单方式登录
                .loginProcessingUrl("/login")	// 用户登录接口,使用表单方式,框架自动验证
                .successHandler(authenticationSuccessHandler)	// 验证成功则执行此处理器
                .failureHandler(authenticationFailureHandler)	// 验证失败则执行此处理器
                .and()
                .authorizeRequests()
                .antMatchers("/user/register").permitAll() // 放行策略,用户注册接口放行,无需拦截
                .antMatchers("/swagger-ui.html", "/webjars/**", "/v2/**", "/swagger-resources/**","/doc.html").permitAll()	// 放行swagger的静态资源
                .anyRequest().authenticated();	// 其余接口均需通过验证

        // 在框架验证用户名密码前,加入jwt过滤器,当此用户已登录时跳过验证
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        // 配置登出处理
        http.logout()
                .logoutUrl("/logout") // 指定登出的接口路由
                .logoutSuccessUrl("/index.html") // 登出成功后跳转的页面
                .invalidateHttpSession(true) // 使 HttpSession 失效
                .deleteCookies("JSESSIONID"); // 删除指定的 Cookie,如需要可添加多个
    }
}
3、创建Jwt过滤器,用于判断用户是否拥有token,有则跳过登录验证
@Service
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authorizationHeader = request.getHeader("Authorization");
        String token = null;
        String username = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            token = authorizationHeader.substring(7);
            username = JwtUtils.getUsernameFromToken(token);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            if (JwtUtils.validateToken(token)) {
                // 生成一个包含账号密码和权限的认证信息
                /**
                 * 1、Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象
                 * 2、Credentials:用户凭证,一般是密码
                 * 3、Authorities:用户权限
                 * */
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, null);
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                // 将Authentication存到上下文中
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
		// 放行
        filterChain.doFilter(request, response);
    }
}
4、创建Jwt工具类用于创建token
package com.example.springtransactiondemo.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

public class JwtUtils {
	// 密钥
    private static final String SECRET_KEY = "abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ";
    // 过期时间
    private static final long EXPIRATION_TIME = 1200000;

    // 生成token
    public static String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS256,SECRET_KEY)
                .compact();
    }

    // 通过token获取username
    public static String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();

        return claims.getSubject();
    }
    
	// 验证token是否有效
    public static boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}
5、实现SpringSecurity自带用户信息接口UserDetails
@Data
public class UserDetailsDTO implements UserDetails {
    // 除了用户名、密码外,UserDetails还内置了账户是否可用等其他字段
    private String username;
    private String password;

    // 在 Spring Security 中,无论是用户角色,还是用户权限,都是从这个方法返回
	// 用于获取当前用户权限,待补充
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    // 账户是否过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
	// 账户是否锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 是否可用
    @Override
    public boolean isEnabled() {
        return true;
    }
}
6、实现UserDetailsService接口(用于从数据库读取用户名密码,返回UserDetails后框架自动进行验证)
// TUserAuth 为自己创建的表对象,至少包含username和password;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private ITUserAuthService userAuthService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StringUtils.isBlank(username)) {
            throw new RuntimeException("用户名不能为空!"); // 可改成自定义异常
        }
        TUserAuth userAuth = userAuthService.lambdaQuery().eq(TUserAuth::getUsername,username).one();
        if (Objects.isNull(userAuth)) {
            throw new RuntimeException("用户不存在!");
        }
        UserDetailsDTO dto = new UserDetailsDTO();
        BeanUtil.copyProperties(userAuth,dto);
        return dto;
    }
}
7、成功和失败处理器
// 成功处理器
@Service
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        UserDetailsDTO dto = (UserDetailsDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String token = null;
        if (Objects.nonNull(authentication)) {
            // 从当前请求中获取用户信息
            UserDetailsDTO userDetailsDTO = (UserDetailsDTO) authentication.getPrincipal();
            // 创建token
            token = JwtUtils.generateToken(userDetailsDTO.getUsername());
        }
        // 将用户登录信息写入返回中
        httpServletResponse.setContentType("application/json;charset=utf-8");
        httpServletResponse.getWriter().write(token);
        // todo 写入数据库等操作
    }
}

// 失败处理器
@Service
public class AuthenticationFailHandlerImpl implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=utf-8");
        httpServletResponse.getWriter().write(e.getMessage());
    }
}
8、Post请求访问登录接口,携带username和password(默认参数名)
	// 若想重命名参数名,则可在SecurityConfig配置文件表单中配置参数名称
	http.csrf().disable()
                .formLogin()
                .loginProcessingUrl("/login")
                .usernameParameter("account") 	// 重命名用户名
                .passwordParameter("pwd")		// 重命名密码
    			...
9、注册时使用框架对密码加密

​ 使用BCrypt.hashpw(password, BCrypt.gensalt())进行密码加密再存入数据库;

@Override
public boolean register(String username, String password) {
    TUserAuth userAuth = userService.lambdaQuery().eq(TUserAuth::getUsername,username).one();
    if(ObjectUtil.isNotNull(userAuth)){
        throw new RuntimeException("该用户名已注册");
    }
    TUserAuth curUser = new TUserAuth();
    curUser.setUsername(username);
    curUser.setPassword(BCrypt.hashpw(password, BCrypt.gensalt()));
    return userService.saveOrUpdate(curUser);
}
10、开启权限认证

(1)SecurityConfig配置类中启用注解机制;

// 添加此注解,开启方法级权限认证
@EnableGlobalMethodSecurity(prePostEnabled = true)

(2)UserDetailsDTO中加入权限列表;

@Data
public class UserDetailsDTO implements UserDetails {
    private long id;
    private String username;
    private String password;
	// 权限列表
    private List<String> authorities;
    // 重写get权限方法
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return ObjectUtil.isNull(this.authorities) ? null:this.authorities.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toSet());
    }
    ...
}

(3)UserDetailsService实现类中从数据库获取权限列表;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private ITUserAuthService userAuthService;
    @Resource
    private TUserAuthMapper userAuthMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StringUtils.isBlank(username)) {
            throw new RuntimeException("用户名不能为空!");
        }
        TUserAuth userAuth = userAuthService.lambdaQuery().eq(TUserAuth::getUsername,username).one();
        if (Objects.isNull(userAuth)) {
            throw new RuntimeException("用户不存在!");
        }
        UserDetailsDTO dto = new UserDetailsDTO();
        BeanUtil.copyProperties(userAuth,dto);
        // 此处获取数据库中该role的权限列表,加进当前UserDetails
        List<String> authorities = userAuthMapper.selectUserAuthorities(dto.getId());
        dto.setAuthorities(authorities);
        return dto;
    }
}

(4)AuthenticationSuccessHandlerImpl成功处理器中的返回增加user的权限列表,可供前端显示和调用;

@Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        // 从上下文中获取用户信息
        UserDetailsDTO dto = (UserDetailsDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String token = null;
        if (Objects.nonNull(authentication)) {
            // 从当前请求中获取用户信息
            UserDetailsDTO userDetailsDTO = (UserDetailsDTO) authentication.getPrincipal();
            // 创建token
            token = "Bearer "+JwtUtils.generateToken(userDetailsDTO.getUsername());
        }
        LoginUser user = new LoginUser();
        user.setId(dto.getId());
        user.setUsername(dto.getUsername());
        user.setToken(token);
        user.setAuthorities(dto.getAuthorities());
        // 将用户登录信息写入返回中
        httpServletResponse.setContentType("application/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(ResultVO.ok(user)));
        ...
    }

在这里插入图片描述

(5)接口中加入注解@PreAuthorize("hasAuthority('user:getOne')");(可像若依自定义认证@ss)

​ 若用户无此权限,则无法访问。

在这里插入图片描述

11、禁用Session,每次请求必须携带token

(1)SecurityConfig中禁用session;

http.sessionManagement()
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS);// 禁用session,每次必须携带token

(2)使用redis存储UserDetails信息,认证时将权限赋予当前用户;

​ 1)AuthenticationSuccessHandlerImpl中:

// 写入redis数据库,dto即UserDetailsDTO
redisService.hSet("login_user",user.getUsername(),dto,1200);

​ 2)JwtAuthenticationFilter拦截器中:

	@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authorizationHeader = request.getHeader("Authorization");

        String token = null;
        String username = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            token = authorizationHeader.substring(7);
            if(JwtUtils.validateToken(token)) {
                username = JwtUtils.getUsernameFromToken(token);
            }
        }
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            // 从redis中获取当前用户信息
            UserDetailsDTO user = (UserDetailsDTO) redisService.hGet("login_user",username);
            if(ObjectUtil.isNull(user)){
                // 若redis中没有,则放行去登录
                filterChain.doFilter(request, response);
                return;
            }
            // getAuthorities赋予当前用户权限
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, user.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        filterChain.doFilter(request, response);
    }

(3)UserDetailsDTOgetAuthorities序列化和反序列化问题

  • 方法一:直接将UserDetailsDTO存入redis中(需加入@Transient注解)
	/**
     * GrantedAuthority没有默认的无参构造函数,无法直接使用FastJson进行反序列化;
     * 因此,使用redis存储时,反序列化会导致authorities为空值;
     * AuthenticationSuccessHandler处理器无法从redis中获取authorities并赋予当前用户权限;
     * 当SecurityConfig禁用Session时,会导致被判定为无权限;
     * 因此,需加入@Transient注解,使redis存储时不对其进行序列化;
     * */
    @Override
    @Transient
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return ObjectUtil.isNull(this.authorities) ? null:this.authorities.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toSet());
    }
	...
	// 其他除用户名密码外的get方法全部加入@Transient注解
  • 方法二:存入自定义用户信息对象LoginUser(建议使用此种,方便自己定义redis中存储的数据)
// Step1: LoginUser
@Data
public class LoginUser {
    private long id;
    private String username;
    private List<String> authorities;
    private String token;
}



// Step2: AuthenticationSuccessHandlerImpl

	...
    List<String> list = dto.getAuthorities()
        .stream()
        .map(GrantedAuthority::getAuthority)
        .collect(Collectors.toList());	// 将GrantedAuthority集合类型的authorities转换为List<String>类型
    user.setAuthorities(list);
    ...
    // 写入redis数据库
    redisService.hSet("login_user",user.getUsername(),user,1200);
	...


// Step3: 创建redis序列化配置类。
// 不创建时redis会报错:user无法序列化,当然LoginUser实现Serializable接口也可,不过还是配置类更方便,全局配置。
@SpringBootConfiguration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //创建一个json的序列化对象
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        //设置value的序列化方式json
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        //设置key序列化方式String
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置hash key序列化方式String
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //设置hash value序列化json
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        // 设置支持事务
        redisTemplate.setEnableTransactionSupport(true);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public RedisSerializer<Object> redisSerializer() {
        //创建JSON序列化器
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //必须设置,否则无法将JSON转化为对象,会转化成Map类型
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        return new GenericJackson2JsonRedisSerializer(objectMapper);
    }
}



// Step4:JwtAuthenticationFilter
	...
    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            ...
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username
                    , null
                    , loginUser.getAuthorities()	// 再将List<String>类型转回GrantedAuthority集合类型即可
                    .stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toSet()));
            ...
    }
    filterChain.doFilter(request, response);
12、用户无感知刷新token过期时间
  1. Redis 中 token 不存在:
    这种情况表明自用户上次活动以来,缓存中的token已经过期或被清除,可能由于用户长时间未操作或系统缓存策略导致。此时,可以认为用户账户处于空闲超时状态,Filter应返回“用户信息已失效,请重新登录”的提示,并引导用户重新进行登录操作。

  2. Redis 中 token 存在:
    如果缓存中存在对应的token,则需要进一步使用JWT工具类来验证这个cache token是否已过期。

    • 未过期:如果cache token未过期,说明用户当前是活跃的,无需进行额外处理,继续处理请求即可。
    • 已过期:如果cache token已过期,但考虑到用户可能一直在进行操作只是token本身失效了,后端程序可以执行以下操作:首先,根据当前用户信息(可能需要从数据库或其他认证服务中获取)重新生成一个新的JWT token;然后,使用这个新的token覆盖缓存中原有的token值;最后,更新该缓存项的生命周期,重新开始计算。这样,用户在无需重新登录的情况下,可以继续操作。
13、用户退出(get/post请求都可,登出时让前端携带token)

(1)SecurityConfig中配置登出;

	// 配置登出处理
    http.logout()
        .logoutUrl("/logout") // 指定登出的接口路由
        .invalidateHttpSession(true) // 使 HttpSession 失效
        .logoutSuccessHandler(logoutSuccessHandler) // 成功退出处理器
        .deleteCookies("JSESSIONID"); // 删除指定的 Cookie,如需要可添加多个

(2)登出成功处理器

@Service
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
    @Resource
    private RedisService redisService;
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
		// 登出时让前端携带token
        String token = httpServletRequest.getHeader("Authorization").substring(7);
        // 使token失效
        if(JwtUtils.validateToken(token)){
            String username = JwtUtils.getUsernameFromToken(token);
            // 删除redis中保存用户信息,下次用户使用该token请求时使其重新登录
            redisService.hDel("login_user",username);
        }
        httpServletResponse.setContentType("application/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(ResultVO.ok("退出成功")));
    }

(3)用户使用token进行接口调用时,拦截器中判断Redis中该用户信息的token是否和该token相同,若相同则正常返回,否则则意味这用户重新登录过,该token失效;(保证Redis中存的是最新且唯一有效的token)

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            LoginUser loginUser = (LoginUser) redisService.hGet("login_user",username);
            ...
            if(!loginUser.getToken().equals("Bearer "+token)){
                filterChain.doFilter(request, response);
                return;
            }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值