SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统

Posted 殷丿grd_志鹏

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统相关的知识,希望对你有一定的参考价值。

先参考文章https://blog.csdn.net/grd_java/article/details/107584578,了解springSecurity基本操作,另外,数据库也在这篇文章创建了

基本效果说明
  1. 请求login接口,获取token
  2. 获取token后,通过封装名为Authorization的请求头,值为Bearer+空格+token字符串的形式请求需要授权接口(就是请求时,需要带上authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE2Mzk0NjA2MTYsImV4cCI6MTYzOTQ2MTMwMCwic3ViIjoiYWRtaW4iLCJjcmVhdGVkIjoxNjM5NDYwNjE2MTI0fQ._oKG2qarbKi84gxdQjHoRSHd3hx-INQn1CscMgAiASW0B6tsPIjWi1LMr35OTtWR-WvQ8R6tRAvkp0Q3RQm0LQ这样的请求头)
  1. authorization是请求头key
  2. Bearer 是我们的一个头标识
  3. 剩下的就是token字符串
源码,码云

@TODO 因为要将springSecurity整个的权限控制流程都涉及到,暂时还没写完,写完会上传到仓库

一、环境说明(太基本的就不讲了,本文主要讲如何前后端分离模式整合Security)

common除了Spring security模块外,都在这篇文章中有讲解https://blog.csdn.net/grd_java/article/details/107452826


不知道怎么建表,请到文章开头的链接中参考

二、配置Security

首先,我们在yml中需要配置一些参数,是我们个人规定好的
  1. 我们规定相应的token 请求头为authorization:Bearer token字符串
jwt:
  tokenHeader: Authorization #JWT存储的请求头,请求是这个就是token的key
  secret: ukc8BDbRigUDaY6pZFfWus2jZWLPHO #JWT加解密使用的密钥
  expiration: 684808 #JWT有效时间(60*60*24)
  tokenHead: Bearer #JWT负载中拿到的开头,token字符串,tokenHead是字符串的头,也就是以它开头

1. 用户实体类继承UserDetails接口

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="DdUser对象,使用Spring Security框架就要继承UserDetails接口,实现方法,将返回值改为true", description="")
public class DdUser implements UserDetails 
    private static final long serialVersionUID=1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.ID_WORKER_STR)
    private Integer id;
    private String username;
    private String password;

    /**
     * 所有权限
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() 
        return null;
    

    /**
     * 账号是否过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() 
        return true;
    

    /**
     * 账号是否被锁定
     * @return
     */
    @Override
    public boolean isAccountNonLocked() 
        return true;
    

    /**
     * 凭证(密码)是否过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() 
        return true;
    

    /**
     * 是否启用
     * @return
     */
    @Override
    public boolean isEnabled() 
        return true;
    

2. 编写JWT工具类

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JWT工具类
 */
@Component
public class JwtTokenUtil 
    private static final String CLAIM_KEY_USERNAME="sub";//荷载,用户名的key
    private static final String CLAIM_KEY_CREATED="created";//荷载,创建时间key
    /**
     * 项目中推荐使用此形式的密钥和过期时间,因为解耦合
     */
    @Value("$jwt.secret")
    private String secret;//密钥
    @Value("$jwt.expiration")
    private Long expiration;//过期时间

    /**
     * 不推荐使用这种,因为写死到了代码中
     */
    public static final long EXPIRE = 1000 * 60 * 60 * 24;                      //设置token过期时间
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";   //设置token密钥,我瞎写的,每个公司都有按自己规则生成的密钥

    //生成token字符串

    /**
     * 根据荷载生成token字符
     * @param id 密言1
     * @param nickname 密言2
     * @return
     */
    public static String getJwtToken(String id, String nickname)

        String JwtToken = Jwts.builder()                                          //构建jwt字符串
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")                             //设置jwt头信息

                .setSubject("guli-user")                                           //分类,名字随便起的,不同的分类可以设置不同的过期
                .setIssuedAt(new Date())                                           //设置过期时间的计时起始值为当前时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))      //设置过期时间为当前时间+EXPIRE我们设定的过期时间

                .claim("id", id)                                                //token主体,这里放你需要的信息,我们实现登陆,就放用户登陆信息
                .claim("nickname", nickname)                                    //需要多少主体信息,就设置多少个claim属性

                .signWith(SignatureAlgorithm.HS256, APP_SECRET)                    //签名哈希,根据指定规则和我们的密钥设定签名
                .compact();

        return JwtToken;
    

    /**
     * 根据spring security 对象生成token
     * @param userDetails UserDetails对象,spring security会将用户信息保存在这个对象
     * @return
     */
    public String getJwtToken(UserDetails userDetails)

        //分组荷载,也就是主体信息,我们将用户名和有效时间封装到token中
        Map<String,Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED,new Date());

        String JwtToken = gennerateToken(claims);

        return JwtToken;
    
    private String gennerateToken(Map<String,Object> claims)
        String JwtToken =Jwts.builder()                                          //构建jwt字符串
//                .setHeaderParam("typ", "JWT")
//                .setHeaderParam("alg", "HS256")                             //设置jwt头信息
//
//                .setSubject("dd-user")                                             //分类,名字随便起的,不同的分类可以设置不同的过期
                .setIssuedAt(new Date())                                           //设置过期时间的计时起始值为当前时间
                .setExpiration(new Date(System.currentTimeMillis() + expiration))  //设置过期时间为当前时间+EXPIRE我们设定的过期时间
                .addClaims(claims)
//                .claim(claims)                          //token主体,这里放你需要的信息,我们实现登陆,就放用户登陆信息
//                .claim()                               //需要多少主体信息,就设置多少个claim属性
                .signWith(SignatureAlgorithm.HS512, secret)                    //签名哈希,根据指定规则和我们的密钥设定签名
                .compact();
        return JwtToken;
    
    /**
     * 从token中获取登录用户名
     */
    public String getUsernameByToken(String token)
        String username;

        try 
            Claims claims = getClaimsFormToken(token); //获取主体内容
            //claims.get(CLAIM_KEY_USERNAME);
            username = claims.getSubject();//获取主体值,上面注释代码和这个都可以,获取的是CLAIM_KEY_USERNAME常量封装的key,对应的值
        catch (Exception e)
            username = null;
            e.printStackTrace();
        
        return username;

    

    /**
     * 从token中获取荷载
     * @param token
     * @return
     */
    private Claims getClaimsFormToken(String token)
        Claims claims = null;
        try 
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();//解析token字符串,获取主体内容
        catch (Exception e)
            e.printStackTrace();
        
        return claims;
    

    /**
     * 从荷载中获取过期时间
     */
    private Date getExpiredDateFromToken(String token)
        Claims claimsFormToken = getClaimsFormToken(token);
        return claimsFormToken.getExpiration();
    
    /**
     * 验证token是否失效,失效返回true,没失效返回false
     */
    private boolean isTokenExpired(String token)
        Date expiredDateFromToken = getExpiredDateFromToken(token);
//        boolean before = expiredDateFromToken.before(new Date());
        return expiredDateFromToken.before(new Date());//如果当前时间再过期时间之前,就是还没有到过期时间,返回true,否则返回false
    

    /**
     * 判断token是否有效
     * @param jwtToken token字符串
     * @return
     */
    public boolean checkToken(String jwtToken,UserDetails userDetails) 
        if(StringUtils.isEmpty(jwtToken)) return false;//如果为空直接返回false表示失效
        String username;
        try 
             username = getUsernameByToken(jwtToken);//根据token获取用户名
         catch (Exception e) 
            e.printStackTrace();
            username = null;
            return false;//获取不到用户名,直接失效
        
        if(!username.equals(userDetails.getUsername())) return false;//如果token的用户名与当前UserDetails用户名不一致,直接失效
        if(isTokenExpired(jwtToken)) return false;//如果token失效,返回false
        return true;
    
    /**
     * 判断token是否可以被刷新
     */
    public boolean canRefresh(String token)
        return !isTokenExpired(token);//已经过期,表示可以被刷新
    

    /**
     * 刷新token
     */
    public String refreshToken(String token)
        Claims claimsFormToken = getClaimsFormToken(token);
        claimsFormToken.put(CLAIM_KEY_CREATED,new Date());
        return gennerateToken(claimsFormToken);
    

3. 配置JWT过滤器

任何请求都需要进行过滤,如果请求携带我们规定的token,那么将token放在spring Security对象中

import com.dd.security.utils.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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;
import java.util.Enumeration;

/**
 * JWT登录授权过滤器,继承OncePerRequestFilter
 */
public class JwtAuthencationTokenFilter extends OncePerRequestFilter 


    @Value("$jwt.tokenHeader")
    private String tokenHeader;//JWT存储的请求头

    @Value("$jwt.tokenHead")
    private String tokenHead;//JWT负载中拿到的开头,token字符串,tokenHead是字符串的头,也就是以它开头
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 前置拦截
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException 
//        Enumeration<String> headerNames = request.getHeaderNames();//debug用的
        //通过tokenHeader获取验证头,也就是tokenHead+token字符串
        String authHeader = request.getHeader(tokenHeader);

        //如果验证头存在,并且token字符串符合我们规定的tokenHead开头
        if(authHeader!=null && authHeader.startsWith(tokenHead))
            //将token截取出来
            String authToken = authHeader.substring(tokenHead.length());
            //根据token获取用户名
            String username = jwtTokenUtil.getUsernameByToken(authToken);
            //如果token存在,但是Security全局上下文没有用户,表示用户没有登录
            if(username != null && SecurityContextHolder.getContext().getAuthentication() == null)
                //登录
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                //如果token有效
                if(jwtTokenUtil.checkToken(authToken,userDetails))
                    //更新登录用户对象UserDetails
                    UsernamePasswordAuthenticationToken authenticationToken =
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    //重新设置到用户对象中
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    //设置到security全局上下文
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                
            
        

        filterChain.doFilter(request,response);//放行
    

4. 自定义返回结果

当访问接口没有权限,自定义返回结果

import com.dd.common_utils.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet

以上是关于SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统的主要内容,如果未能解决你的问题,请参考以下文章

SpringSecurity如何实现前后端分离

SpringSecurity解决跨域问题,在SpringBoot整合SprinSecurity中如何用前后端分离Ajax登录,Ajax登录返回状态200还是近error

Spring boot+Spring security+JWT实现前后端分离登录认证及权限控制

SpringBoot2.0.3 + SpringSecurity5.0.6 + vue 前后端分离认证授权

springboot+springsecurity+vue+springdatajpa前后端分离员工管理系统

spring boot整合jwt 实现前后端分离登录认证及授权