将SpringBoot+SpringSecurity改造为前后端分离+Jwt的权限认证系统,Token过期刷新问题

补充 2024年11月13日
其实除了jwt方案, 要实现分布式的统一登录状态, redis作为session管理也是不错的方案.
通过引入依赖 spring-session-data-redis, 里面的RedisOperationsSessionRepository会实现session从redis的存储和取出. CookieHttpSessionIdResolver可以使用cookie作为sessionId的存储, 也可以自定义实现sessionId的解析器来决定是把sessionId放在哪里. 这个可以查询这方面的资料, 目前工作中用的模式是这种

前言

一般来说,我们用SpringSecurity默认的话是前后端整在一起的,比如thymeleaf或者Freemarker,SpringSecurity还自带login登录页,还让你配置登出页,错误页。
但是现在前后端分离才是正道,前后端分离的话,那就需要将返回的页面换成Json格式交给前端处理了

SpringSecurity默认的是采用Session来判断请求的用户是否登录的,但是不方便分布式的扩展,虽然SpringSecurity也支持采用SpringSession来管理分布式下的用户状态,不过现在分布式的还是无状态的Jwt比较主流。 所以下面说下怎么让SpringSecurity变成前后端分离,采用Jwt来做认证的

一、五个handler一个filter两个User

5个handler,分别是

  • 实现AuthenticationEntryPoint接口,当匿名请求需要登录的接口时,拦截处理
  • 实现AuthenticationSuccessHandler接口,当登录成功后,该处理类的方法被调用
  • 实现AuthenticationFailureHandler接口,当登录失败后,该处理类的方法被调用
  • 实现AccessDeniedHandler接口,当登录后,访问接口没有权限的时候,该处理类的方法被调用
  • 实现LogoutSuccessHandler接口,注销的时候调用

1.1 AuthenticationEntryPoint

匿名未登录的时候访问,遇到需要登录认证的时候被调用

/**
 * 匿名未登录的时候访问,需要登录的资源的调用类
 * @author zzzgd
 */
@Component
public class CustomerAuthenticationEntryPoint implements AuthenticationEntryPoint {
   
   
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
   
   
	    //设置response状态码,返回错误信息等
	    ...
        ResponseUtil.out(401, ResultUtil.failure(ErrorCodeConstants.REQUIRED_LOGIN_ERROR));
    }
}

1.2 AuthenticationSuccessHandler

这里是我们输入的用户名和密码登录成功后,调用的方法
简单的说就是获取用户信息,使用JWT生成token,然后返回token


/**
 * 登录成功处理类,登录成功后会调用里面的方法
 * @author Exrickx
 */
@Slf4j
@Component
public class CustomerAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
   
   


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
   
   
    	//简单的说就是获取当前用户,拿到用户名或者userId,创建token,返回
        log.info("登陆成功...");
        CustomerUserDetails principal = (CustomerUserDetails) authentication.getPrincipal();
        //颁发token
        Map<String,Object> emptyMap = new HashMap<>(4);
        emptyMap.put(UserConstants.USER_ID,principal.getId());
        String token = JwtTokenUtil.generateToken(principal.getUsername(), emptyMap);
        ResponseUtil.out(ResultUtil.success(token));
    }
}

1.3 AuthenticationFailureHandler

有登陆成功就有登录失败
登录失败的时候调用这个方法,可以在其中做登录错误限制或者其他操作,我这里直接就是设置响应头的状态码为401,返回


/**
 * 登录账号密码错误等情况下,会调用的处理类
 * @author Exrickx
 */
@Slf4j
@Component
public class CustomerAuthenticationFailHandler implements AuthenticationFailureHandler {
   
   


    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
   
   
    //设置response状态码,返回错误信息等
    	....
        ResponseUtil.out(401, ResultUtil.failure(ErrorCodeConstants.LOGIN_UNMATCH_ERROR));
    }

}

1.4 LogoutSuccessHandler

登出注销的时候调用,Jwt有个缺点就是无法主动控制失效,可以采用Jwt+session的方式,比如删除存储在Redis的token

这里需要注意,如果将SpringSecurity的session配置为无状态,或者不保存session,这里authentication为null!! ,注意空指针问题。(详情见下面的配置WebSecurityConfigurerAdapter)

/**
 * 登出成功的调用类
 * @author zzzgd
 */
@Component
public class CustomerLogoutSuccessHandler implements LogoutSuccessHandler {
   
   
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
   
   
        ResponseUtil.out(ResultUtil.success("Logout Success!"));
    }
}

1.5 AccessDeniedHandler

登录后,访问缺失权限的资源会调用。

/**
 * 没有权限,被拒绝访问时的调用类
 * @author Exrickx
 */
@Component
@Slf4j
public class CustomerRestAccessDeniedHandler implements AccessDeniedHandler {
   
   

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
   
   
        ResponseUtil.out(403, ResultUtil.failure(ErrorCodeConstants.PERMISSION_DENY));
    }

}

1.6 一个过滤器OncePerRequestFilter

这里算是一个小重点。
上面我们在登录成功后,返回了一个token,那怎么使用这个token呢?

前端发起请求的时候将token放在请求头中,在过滤器中对请求头进行解析。

  1. 如果有accessToken的请求头(可以自已定义名字),取出token,解析token,解析成功说明token正确,将解析出来的用户信息放到SpringSecurity的上下文中
  2. 如果有accessToken的请求头,解析token失败(无效token,或者过期失效),取不到用户信息,放行
  3. 没有accessToken的请求头,放行

这里可能有人会疑惑,为什么token失效都要放行呢?
这是因为SpringSecurity会自己去做登录的认证和权限的校验,靠的就是我们放在SpringSecurity上下文中的SecurityContextHolder.getContext().setAuthentication(authentication);,没有拿到authentication,放行了,SpringSecurity还是会走到认证和校验,这个时候就会发现没有登录没有权限。

旧版本, 最新在底部

package com.zgd.shop.web.config.auth.filter;

import com.zgd.shop.common.constants.SecurityConstants;
import com.zgd.shop.common.util.jwt.JwtTokenUtil;
import com.zgd.shop.web.config.auth.user.CustomerUserDetailService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 过滤器,在请求过来的时候,解析请求头中的token,再解析token得到用户信息,再存到SecurityContextHolder中
 * @author zzzgd
 */
@Component
@Slf4j
public class CustomerJwtAuthenticationTokenFilter extends OncePerRequestFilter {
   
   

    @Autowired
    CustomerUserDetailService customerUserDetailService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
   
   
        
    	//请求头为 accessToken
    	//请求体为 Bearer token

    	String authHeader = request.getHeader(SecurityConstants.HEADER);

        if (authHeader != null && authHeader.startsWith(SecurityConstants.TOKEN_SPLIT)) {
   
   

            final String authToken = authHeader.substring(SecurityConstants.TOKEN_SPLIT.length());
            String username = JwtTokenUtil.parseTokenGetUsername(authToken);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
   
   
                UserDetails userDetails = customerUserDetailService.loadUserByUsername(username);
                if (userDetails != null) {
   
   
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

1.7 实现UserDetails扩充字段

这个接口表示的用户信息,SpringSecurity默认实现了一个User,不过字段寥寥无几,只有username,password这些,而且后面获取用户信息的时候也是获取的UserDetail。

于是我们将自己的数据库的User作为拓展,自己实现这个接口。继承的是数据库对应的User,而不是SpringSecurity的User

package com.zgd.shop.web.config.auth.user;

import com.zgd.shop.common.constants.UserConstants;
import com.zgd.shop.dao.entity.model.User;
import org.springframework.security.core.GrantedAuthority;
import 
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值