前后端分离Spring Security + Redis + JWT 实现动态权限管理

2022年7月26日13:14:40

项目环境

本篇文章环境:Spring Boot + Mybatis + Spring Security + Redis + JWT

预期成果:实现具有验证码校验、RBAC 权限控制的前后端分离项目

注意:为什么要使用 Json Web Token(JWT)?这是因为前后端分离项目中,前端与后端之间的通信采用 RESTFul API 的交互方式进行交互。这种前后端分离的交互是无状态的交互方式,所以,每次交互都必须进行身份验证。而传统方案是根据用户信息生成token,将token 存入浏览器 cookie(或存入数据库),之后每次请求都会带上这个 cookie,由后端根据这个 cookie 来查询用户并验证是否过期。这种方案存在很多的问题,由于 cookie 是可以被 JavaScript 读取的,这会导致用户 token 泄露。

数据库设计

Web 的安全控制一般分为两个部分,一个是认证,一个是授权。认证即判断是否为合法用户,简单的说就是登录。用户名和密码匹配成功即认证成功。授权是基于已认证的前提下,根据用户的不同权限,开放不同的资源(本文简单化处理,认为资源就是 API,实际的资源可能包括菜单、静态图片等)。一般 RBAC 权限控制有三层,即:用户<–>角色<–>权限,用户与角色是多对多,角色和权限也是多对多,最后由权限控制资源(URL)的访问。本文为了便于处理,在数据库设计时对表内的字段进行了简化。
前后端分离Spring Security + Redis + JWT 实现动态权限管理

在这里我们先暂时不考虑权限,只考虑用户<–>角色<–>资源。(源码被注释的部分中含有用户、角色、权限三层控制)

认证管理

在这一步中,我们需要自定义UserDetailsService ,将用户信息和权限注入进来,为后面的授权做准备。

在实现UserDetailsService之后,需要重写loadUserByUsername 方法,参数是用户输入的用户名。返回值是UserDetails,这是一个接口,一般使用它的子类org.springframework.security.core.userdetails.User,它有三个参数,分别是用户名、密码和权限集。(实际开发中,我们可以将实体类中的 User 继承org.springframework.security.core.userdetails.User以满足更多需求)

并且实际应用中,为了减少对数据库的访问次数,我们通常会将权限集放入缓存中,下次可以直接从缓存中获取,可以有效提高效率。

UserDetailsService 实现类

packagecom.security.service;importcom.security.mapper.APIMapper;importcom.security.mapper.AuthoritiesMapper;importcom.security.mapper.RoleMapper;importcom.security.mapper.UserMapper;importcom.security.pojo.SysUser;importcom.security.utils.RedisUtils;importorg.apache.commons.lang3.StringUtils;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.core.authority.AuthorityUtils;importorg.springframework.security.core.authority.SimpleGrantedAuthority;importorg.springframework.security.core.userdetails.User;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.stereotype.Service;importjava.util.ArrayList;importjava.util.List;@Service("userDetailsService")publicclassUserDetailServiceImplimplementsUserDetailsService{@AutowiredprivatePasswordEncoder passwordEncoder;@AutowiredprivateUserMapper userMapper;@AutowiredprivateRoleMapper roleMapper;@AutowiredprivateAuthoritiesMapper authoritiesMapper;@AutowiredprivateRedisUtils redisUtils;//自定义的登录逻辑@OverridepublicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{SysUser user= userMapper.queryUserByUsername(username);//根据用户名去数据库进行查询,如不存在则抛出异常if(user==null){thrownewUsernameNotFoundException("用户不存在");}List<GrantedAuthority> authorities=newArrayList<>();//方法一:使用用户、角色、资源建立关系,直接使用角色控制权限List<String> codeList= roleMapper.queryUserRole(user.getUsername());//添加权限信息进入缓存
        redisUtils.set(username,StringUtils.join(codeList,","),60*60);//方法二:添加权限(资源表),通过建立用户、角色、权限、资源之间的关系,使用"权限"实现按钮级别的权限控制//        List<String> codeList = authoritiesMapper.queryAuthoritiesList(user.getUsername());
        codeList.forEach(code->{SimpleGrantedAuthority simpleGrantedAuthority=newSimpleGrantedAuthority(code);
            authorities.add(simpleGrantedAuthority);});returnnewUser(username, user.getPassword(), authorities);}}

部分 mapper 接口和具体实现

packagecom.security.mapper;importcom.security.pojo.Role;importorg.apache.ibatis.annotations.Mapper;importorg.apache.ibatis.annotations.Param;importorg.springframework.stereotype.Repository;importjava.util.List;@Mapper@RepositorypublicinterfaceRoleMapper{List<String>queryUserRole(@Param("username")String username);List<Role>selectListByUrl(String url);}
packagecom.security.mapper;importcom.security.pojo.SysUser;importorg.apache.ibatis.annotations.Mapper;importorg.apache.ibatis.annotations.Param;importorg.springframework.stereotype.Repository;@Mapper@RepositorypublicinterfaceUserMapper{SysUserqueryUserByUsername(@Param("username")String username);}
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.security.mapper.RoleMapper"><selectid="queryUserRole"resultType="String">
        select
            r.code
        from
            user_role ur
            left join sys_user u on ur.user_id = u.id
            left join sys_role r on ur.role_id = r.id
        where
            u.username = #{username}</select><selectid="selectListByUrl"resultType="Role">
        SELECT
            r.*
        FROM
            sys_role r
        LEFT JOIN sys_role_api ra ON ra.role_id = r.id
        LEFT JOIN sys_api sa ON sa.id = ra.api_id
        WHERE sa.api_url = #{url}</select></mapper>
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.security.mapper.UserMapper"><selectid="queryUserByUsername"resultType="SysUser">
        select * from sys_user where username = #{username}</select></mapper>

接着,创建一个配置类并继承自WebSecurityConfigurerAdapter,并重写configure(AuthenticationManagerBuilder auth) 方法就可以完成简单的登录认证了。在这一步里,我们需要自定义两个处理器,分别是成功处理器和失败处理器,以及统一的前后端通信消息体格式。

成功处理器

packagecom.security.config;importcom.security.common.ResponseBody;importcom.security.utils.JwtUtils;importcom.security.utils.ResponseBodyUtil;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.security.core.Authentication;importorg.springframework.security.web.authentication.AuthenticationSuccessHandler;importorg.springframework.stereotype.Component;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;@ComponentpublicclassAjaxAuthenticationSuccessHandlerextendsJSONAuthenticationimplementsAuthenticationSuccessHandler{//JWT处理工具类@AutowiredprivateJwtUtils jwtUtils;@OverridepublicvoidonAuthenticationSuccess(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,Authentication authentication)throwsIOException,ServletException{ResponseBody responseBody=ResponseBodyUtil.success();//生成jwt TokenString jwt= jwtUtils.generateToken(authentication.getName());
        httpServletResponse.setHeader(jwtUtils.getHeader(), jwt);//继承封装的输出JSON格式类,并调用父类方法即可this.WriteJSON(httpServletRequest,httpServletResponse,responseBody);}}

失败处理器

packagecom.security.config;importcom.security.common.ResponseBody;importcom.security.common.ResponseCode;importcom.security.utils.ResponseBodyUtil;importorg.springframework.security.authentication.*;importorg.springframework.security.core.AuthenticationException;importorg.springframework.security.web.authentication.AuthenticationFailureHandler;importorg.springframework.stereotype.Component;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;@ComponentpublicclassAjaxAuthenticationFailureHandlerextendsJSONAuthenticationimplementsAuthenticationFailureHandler{@OverridepublicvoidonAuthenticationFailure(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,AuthenticationException e)throwsIOException,ServletException{ResponseBody responseBody=null;if(einstanceofAccountExpiredException){//账号过期
            responseBody=ResponseBodyUtil.fail(ResponseCode.USER_ACCOUNT_EXPIRED);}elseif(einstanceofBadCredentialsException){//密码错误
            responseBody=ResponseBodyUtil.fail(ResponseCode.USER_CREDENTIALS_ERROR);}elseif(einstanceofCredentialsExpiredException){//密码过期
            responseBody=ResponseBodyUtil.fail(ResponseCode.USER_CREDENTIALS_EXPIRED);}elseif(einstanceofDisabledException){//账号不可用
            responseBody=ResponseBodyUtil.fail(ResponseCode.USER_ACCOUNT_DISABLE);}elseif(einstanceofLockedException){//账号锁定
            responseBody=ResponseBodyUtil.fail(ResponseCode.USER_ACCOUNT_LOCKED);}elseif(einstanceofInternalAuthenticationServiceException){//用户不存在
            responseBody=ResponseBodyUtil.fail(ResponseCode.USER_ACCOUNT_NOT_EXIST);}else{//其他错误
            responseBody=ResponseBodyUtil.fail(ResponseCode.COMMON_FAIL);}//继承封装的输出JSON格式类,并调用父类方法即可this.WriteJSON(httpServletRequest,httpServletResponse,responseBody);}}

封装的 JSONAuthentication 抽象类

该抽象类主要是对处理器内都需要实现的一些功能的一个封装

packagecom.security.config;importcom.alibaba.fastjson.JSON;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;importjava.io.PrintWriter;publicabstractclassJSONAuthentication{/**
     * 输出JSON格式数据
     * @param httpServletRequest
     * @param httpServletResponse
     * @param obj
     * @throws IOException
     * @throws ServletException
     */protectedvoidWriteJSON(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,Object obj)throwsIOException,ServletException{//设置编码格式
        httpServletResponse.setContentType("text/json;charset=utf-8");//处理跨域问题
        httpServletResponse.setHeader("Access-Control-Allow-Origin","*");
        httpServletResponse.setHeader("Access-Control-Allow-Methods","POST, GET");//输出JSONPrintWriter out= httpServletResponse.getWriter();
        out.write(JSON.toJSONString(obj));
        out.flush();
        out.close();}}

封装的消息体

packagecom.security.common;importlombok.Data;importlombok.Getter;importlombok.NoArgsConstructor;importlombok.Setter;importjava.io.Serializable;@Data@Getter@Setter@NoArgsConstructorpublicclassResponseBody<T>implementsSerializable{privateBoolean success;privateInteger statusCode;privateString msg;privateT data;publicResponseBody(boolean success){this.success= success;this.statusCode= success?ResponseCode.SUCCESS.getCode():ResponseCode.COMMON_FAIL.getCode();this.msg= success?ResponseCode.SUCCESS.getMessage():ResponseCode.COMMON_FAIL.getMessage();}publicResponseBody(boolean success,ResponseCode resultEnum){this.success= success;this.statusCode= success?ResponseCode.SUCCESS.getCode():(resultEnum==null?ResponseCode.COMMON_FAIL.getCode(): resultEnum.getCode());this.msg= success?ResponseCode.SUCCESS.getMessage():(resultEnum==null?ResponseCode.COMMON_FAIL.getMessage(): resultEnum.getMessage());}publicResponseBody(boolean success,T data){this.success= success;this.statusCode= success?ResponseCode.SUCCESS.getCode():ResponseCode.COMMON_FAIL.getCode();this.msg= success?ResponseCode.SUCCESS.getMessage():ResponseCode.COMMON_FAIL.getMessage();this.data= data;}publicResponseBody(boolean success,ResponseCode resultEnum,T data){this.success= success;this.statusCode= success?ResponseCode.SUCCESS.getCode():(resultEnum==null?ResponseCode.COMMON_FAIL.getCode(): resultEnum.getCode());this.msg= success
  • 作者:人生何事不浮云
  • 原文链接:https://blog.csdn.net/qq_36748248/article/details/120932954
    更新时间:2022年7月26日13:14:40 ,共 9995 字。