1. Separation of front and back ends of springSecurity + global exception capture + token squeeze line + token renewal + dynamic resource authorization + verification code + front-end plaintext encryption

springSecurity主要完成认证和授权,认证就是谁可以访问,授权就是访问者可以干什么,我说一下登录的认证流程吧
1.前端发送请求携带用户名和密码访问登录接口
2.校验密码和数据库是否一致
3.如果一致,使用用户名生成一个token返回给前端
4.前端进行存储token,如果此时访问其他请求需要在请求头中携带token,服务器获取token进行解析是否有效,如果有效根据该token获取用户的信息

2. The dependencies and technologies used SpringSecurity+MybatisPuls+JWT+Redis+Mysql

Project address: springSecurity2: springSecurity front-end separation + global exception capture + token squeeze line + token renewal + dynamic resource authorization - Gitee.com

Front-end project address: springsecurityWeb: front-end page 

Demo address: springSecurity demo login->token squeeze line->authentication->dynamic authorization_哔哩哔哩_bilibili

This article mainly introduces which classes the functions in the project are written in, which is convenient for readers to find

1. Authentication+token squeeze out the line+token renewal

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    public JwtAuthenticationTokenFilter(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    private RedisTemplate<String,Object> redisTemplate;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if(Objects.isNull(token)){
            filterChain.doFilter(request,response);
            return;
        }
        String userId = null;
        try {
            //校验token是否有效
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            WebExceptionUtil.authenticateException("token非法",CommonStatus.EXCEPTION);
        }
        //从redis中获取
        String redisKey="loginUser:"+userId;
        LoginUser loginUser= (LoginUser) redisTemplate.opsForValue().get(redisKey);
        if(Objects.isNull(loginUser)){
            WebExceptionUtil.authenticateException("用户未登录",CommonStatus.EXCEPTION);
        }
        //挤掉token,判断是否是最新的token
        if(!loginUser.getNewToken().equals(token)){
            WebExceptionUtil.authenticateException("其他地方已登录,请从新登录",CommonStatus.EXCEPTION);
        }
        //token续期
        Long expire = redisTemplate.opsForValue().getOperations().getExpire(redisKey);
        if(expire==-2){
            WebExceptionUtil.authenticateException("token已过期,请从新登录",CommonStatus.EXCEPTION);
        }else {
            //过期时间不足10分钟,加1小时
            if(expire<60*10){
                redisTemplate.opsForValue().set(redisKey,loginUser,60*60, TimeUnit.SECONDS);
            }
        }
        //存入SecurityContextHolder为了让其他拦截器能识别已经认证通过
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        filterChain.doFilter(request,response);
    }
}
认证:判断当前传入的token是否合法由JWT判断,如果token为空并且请求的地址非放行的地址,将会被Security内部拦截器处理并且返回403认证失败
token挤掉线:判断当前传入的token是否与数据库的token一致,如果不一致就是旧的token,因为新的token在每次用户登录的时候都会存储到redis中
token续期:我对jwt放弃了时间维护,直接把jwt的过期时间设置为永久,让redis来维护这个token的时间,当有请求过来时会判断当前的时间是否小于10分钟如果是,就将时间+1小时,等一小时过后没有新的请求访问,此时数据会被redis删除

Dynamic Resource Authorization

/**
 * 动态权限校验
 */
public class AccessDeniedFilter extends OncePerRequestFilter {
    private MenuDao menuDao;

    public AccessDeniedFilter(MenuDao menuDao) {
        this.menuDao = menuDao;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("token");
        if(Objects.isNull(token)){
            filterChain.doFilter(request,response);
            return;
        }
        //获取数据库所有需要权限的菜单
        List<Menu> menuList = menuDao.selectList(null);
        boolean competence=false;
        for (Menu menu : menuList) {
            String menuUrl=menu.getUrl();
            boolean matches = Pattern.matches(menuUrl, request.getServletPath());
            //当前url是否需要权限 true 需要 : false 不需要
            if(matches){
                competence=true;
                break;
            }
        }
        //当前请求不需要权限直接放行
        if(!competence){
            filterChain.doFilter(request,response);
            return;
        }
        //以下是需要权限的处理
        boolean denied=false;
        Set<String> menus =getUserMenus();
        for (String menuUrl : menus) {
            //当前的url和数据库的url匹配
            denied= Pattern.matches(menuUrl,request.getServletPath());
            //true 匹配上 : false 未匹配
            if(denied){
                break;
            }
        }

        if(!denied){
            ResultJson resultJson = new ResultJson(403,"权限不足,无法访问");
            WebUtils.renderString((HttpServletResponse) response, JSON.toJSONString(resultJson));
        }else{
            filterChain.doFilter(request,response);
        }
    }

    /**
     * 获取当前用户的所有权限
     * @return
     */
    private Set<String> getUserMenus(){
        SysUserDTO sysUser = UserUtil.getUser();
        List<SysRoleDTO> roles = sysUser.getRoles();
        Set<String> menus= new HashSet<>();
        for (SysRoleDTO role : roles) {
            List<Menu> menuList = role.getMenuList();
            for (Menu menu : menuList) {
                menus.add(menu.getUrl());
            }
        }
        return menus;
    }
}
动态权限授权:当有请求过来的时候会先把数据所有权限都查询出来放入list,
到list中遍历当前请求是否存在,如果找不到就说明当前的请求不需要权限即可访问,直接放行,
如果找到了证明是需要相应的权限才可访问,找到之后就获取当前用户的所持有权限放入list2中,
list2遍历当前请求是否存在,如果存在即有权限访问该请求,否则报403权限不足

Global Controller exception capture

/**
 * 处理Controller抛出异常会被该过滤器处理
 */
public class ControllerExceptionFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        try {
            filterChain.doFilter(servletRequest, response);
        } catch (Exception e) {
            e.printStackTrace();
            String causeStr="";
            Throwable var1 = e.getCause();
            if(var1 != null){
                //获取更简洁的错误信息
                Throwable cause = var1.getCause();
                if(cause !=null){
                    causeStr=cause.toString();
                    causeStr=causeStr.replaceAll("\r\n","↓");
                }
            }
            String stackTraceInfo = new StackTraceInfo().stackTraceInfo(e);
              ResultJson resultJson = new ResultJson(500,var1==null?"":var1.getMessage(),stackTraceInfo);
              WebUtils.renderString((HttpServletResponse) response, JSON.toJSONString(resultJson));
        }
    }


}

Configure under the WebSecurityConfig class 


//Controller异常全局捕获
        http.addFilterAfter(new ControllerExceptionFilter(), FilterSecurityInterceptor.class);

我自定义全局异常放到Security最后一个拦截器的后面,也就是说Security框架的拦截器全部走完没有发生错误,
就走我自定义的拦截器,如果Security某个拦截器发生了错误,那么错误就交给Security拦截器自己来处理,
当Security没有错误走到我的拦截器正常的访问了Controller,
如果Controller发生了异常就要往往抛才能被我自定义的拦截器捕获到,
Controller抛的异常不会被Security拦截器捕获是因为Controller上一层拦截器是我自定义的全局拦截器,我自定拦截器上一层才是Security

Interceptor structure diagram

Execution flowchart

verification code

@RestController
public class CommonController {

    @RequestMapping("/vc")
    public void createVerifyCode(HttpServletRequest request, HttpServletResponse response){
        VerifyCodeUtil verifyCodeUtil = new VerifyCodeUtil();
        verifyCodeUtil.createVerifyCode(request,response);
    }
}

 Front-end plaintext encryption

The front-end encryption code needs to import the jsencrypt.min.js file

 var encryptor = new JSEncrypt();
        encryptor.setPublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCBJzjaOJCvy3hciAasAT9H47d/ql4U4b45Uvj5cUpqoxMIy1znrQKFagmbXNdXzpjwElpq6Ve7+JpqQsjTnzXI4ahn8Z2nfQ6TfoiOCy/aCH2dnRhyl9XcetZNfxfCN7dTibj8Ik6b2Th3iEiiBngHU3NxAk6Se11mk7rsCQnR+QIDAQAB");

        $("#form-login").submit(function() {
            $("[name='username']").prop("value",encryptor.encrypt($("[name='username']")[0].value));
            $("[name='password']").prop("value",encryptor.encrypt($("[name='password']")[0].value));
        });

Backend decryption code

public class RSAUtils{
    /**
     * 解密
     * @param str
     * @return
     */
    public static String decode(String str) {
        return decrypt(str);
    }

}

Guess you like

Origin blog.csdn.net/qq_42058998/article/details/125902034