SpringSecurity+JWT实现登录认证和鉴权

1、准备基本环境

使用基础版的项目环境、然后我们在redis中存储用户相关信息,将用户信息存入到token中

导入相关依赖

 1        <!--redis依赖-->
 2        <dependency>
 3            <groupId>org.springframework.boot</groupId>
 4            <artifactId>spring-boot-starter-data-redis</artifactId>
 5        </dependency>
 6        <!--fastjson依赖-->
 7        <dependency>
 8            <groupId>com.alibaba</groupId>
 9            <artifactId>fastjson</artifactId>
10            <version>1.2.33</version>
11        </dependency>
12        <!--jwt依赖-->
13        <dependency>
14            <groupId>io.jsonwebtoken</groupId>
15            <artifactId>jjwt</artifactId>
16            <version>0.9.0</version>
17        </dependency>

导入共通类

  • 统一返回结果类
 1
 2@JsonInclude(JsonInclude.Include.NON_NULL)
 3public class ResponseResult<T> {
 4    /**
 5     * 状态码
 6     */
 7    private Integer code;
 8    /**
 9     * 提示信息,如果有错误时,前端可以获取该字段进行提示
10     */
11    private String msg;
12    /**
13     * 查询到的结果数据,
14     */
15    private T data;
16
17    public ResponseResult(Integer code, String msg) {
18        this.code = code;
19        this.msg = msg;
20    }
21
22    public ResponseResult(Integer code, T data) {
23        this.code = code;
24        this.data = data;
25    }
26
27    public Integer getCode() {
28        return code;
29    }
30
31    public void setCode(Integer code) {
32        this.code = code;
33    }
34
35    public String getMsg() {
36        return msg;
37    }
38
39    public void setMsg(String msg) {
40        this.msg = msg;
41    }
42
43    public T getData() {
44        return data;
45    }
46
47    public void setData(T data) {
48        this.data = data;
49    }
50
51    public ResponseResult(Integer code, String msg, T data) {
52        this.code = code;
53        this.msg = msg;
54        this.data = data;
55    }
56}
  • JWT工具类
  1/**
  2 * JWT工具类
  3 */
  4public class JwtUtil {
  5
  6    //有效期为
  7    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一个小时
  8    //设置秘钥明文
  9    public static final String JWT_KEY = "sangeng";
 10
 11    public static String getUUID(){
 12        String token = UUID.randomUUID().toString().replaceAll("-", "");
 13        return token;
 14    }
 15  
 16    /**
 17     * 生成jtw
 18     * @param subject token中要存放的数据(json格式)
 19     * @return
 20     */
 21    public static String createJWT(String subject) {
 22        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
 23        return builder.compact();
 24    }
 25
 26    /**
 27     * 生成jtw
 28     * @param subject token中要存放的数据(json格式)
 29     * @param ttlMillis token超时时间
 30     * @return
 31     */
 32    public static String createJWT(String subject, Long ttlMillis) {
 33        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
 34        return builder.compact();
 35    }
 36
 37    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
 38        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
 39        SecretKey secretKey = generalKey();
 40        long nowMillis = System.currentTimeMillis();
 41        Date now = new Date(nowMillis);
 42        if(ttlMillis==null){
 43            ttlMillis=JwtUtil.JWT_TTL;
 44        }
 45        long expMillis = nowMillis + ttlMillis;
 46        Date expDate = new Date(expMillis);
 47        return Jwts.builder()
 48                .setId(uuid)              //唯一的ID
 49                .setSubject(subject)   // 主题  可以是JSON数据
 50                .setIssuer("sg")     // 签发者
 51                .setIssuedAt(now)      // 签发时间
 52                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
 53                .setExpiration(expDate);
 54    }
 55
 56    /**
 57     * 创建token
 58     * @param id
 59     * @param subject
 60     * @param ttlMillis
 61     * @return
 62     */
 63    public static String createJWT(String id, String subject, Long ttlMillis) {
 64        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
 65        return builder.compact();
 66    }
 67
 68    public static void main(String[] args) throws Exception {
 69        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
 70        Claims claims = parseJWT(token);
 71        System.out.println(claims);
 72    }
 73
 74    /**
 75     * 生成加密后的秘钥 secretKey
 76     * @return
 77     */
 78    public static SecretKey generalKey() {
 79        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
 80        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
 81        return key;
 82    }
 83  
 84    /**
 85     * 解析
 86     *
 87     * @param jwt
 88     * @return
 89     * @throws Exception
 90     */
 91    public static Claims parseJWT(String jwt) throws Exception {
 92        SecretKey secretKey = generalKey();
 93        return Jwts.parser()
 94                .setSigningKey(secretKey)
 95                .parseClaimsJws(jwt)
 96                .getBody();
 97    }
 98
 99
100}
  • redis工具类
  1/**
  2 * redis 工具类
  3 */
  4@SuppressWarnings(value = { "unchecked", "rawtypes" })
  5@Component
  6public class RedisCache
  7{
  8    @Autowired
  9    public RedisTemplate redisTemplate;
 10
 11    /**
 12     * 缓存基本的对象,Integer、String、实体类等
 13     *
 14     * @param key 缓存的键值
 15     * @param value 缓存的值
 16     */
 17    public <T> void setCacheObject(final String key, final T value)
 18    {
 19        redisTemplate.opsForValue().set(key, value);
 20    }
 21
 22    /**
 23     * 缓存基本的对象,Integer、String、实体类等
 24     *
 25     * @param key 缓存的键值
 26     * @param value 缓存的值
 27     * @param timeout 时间
 28     * @param timeUnit 时间颗粒度
 29     */
 30    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
 31    {
 32        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
 33    }
 34
 35    /**
 36     * 设置有效时间
 37     *
 38     * @param key Redis键
 39     * @param timeout 超时时间
 40     * @return true=设置成功;false=设置失败
 41     */
 42    public boolean expire(final String key, final long timeout)
 43    {
 44        return expire(key, timeout, TimeUnit.SECONDS);
 45    }
 46
 47    /**
 48     * 设置有效时间
 49     *
 50     * @param key Redis键
 51     * @param timeout 超时时间
 52     * @param unit 时间单位
 53     * @return true=设置成功;false=设置失败
 54     */
 55    public boolean expire(final String key, final long timeout, final TimeUnit unit)
 56    {
 57        return redisTemplate.expire(key, timeout, unit);
 58    }
 59
 60    /**
 61     * 获得缓存的基本对象。
 62     *
 63     * @param key 缓存键值
 64     * @return 缓存键值对应的数据
 65     */
 66    public <T> T getCacheObject(final String key)
 67    {
 68        ValueOperations<String, T> operation = redisTemplate.opsForValue();
 69        return operation.get(key);
 70    }
 71
 72    /**
 73     * 删除单个对象
 74     *
 75     * @param key
 76     */
 77    public boolean deleteObject(final String key)
 78    {
 79        return redisTemplate.delete(key);
 80    }
 81
 82    /**
 83     * 删除集合对象
 84     *
 85     * @param collection 多个对象
 86     * @return
 87     */
 88    public long deleteObject(final Collection collection)
 89    {
 90        return redisTemplate.delete(collection);
 91    }
 92
 93    /**
 94     * 缓存List数据
 95     *
 96     * @param key 缓存的键值
 97     * @param dataList 待缓存的List数据
 98     * @return 缓存的对象
 99     */
100    public <T> long setCacheList(final String key, final List<T> dataList)
101    {
102        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
103        return count == null ? 0 : count;
104    }
105
106    /**
107     * 获得缓存的list对象
108     *
109     * @param key 缓存的键值
110     * @return 缓存键值对应的数据
111     */
112    public <T> List<T> getCacheList(final String key)
113    {
114        return redisTemplate.opsForList().range(key, 0, -1);
115    }
116
117    /**
118     * 缓存Set
119     *
120     * @param key 缓存键值
121     * @param dataSet 缓存的数据
122     * @return 缓存数据的对象
123     */
124    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
125    {
126        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
127        Iterator<T> it = dataSet.iterator();
128        while (it.hasNext())
129        {
130            setOperation.add(it.next());
131        }
132        return setOperation;
133    }
134
135    /**
136     * 获得缓存的set
137     *
138     * @param key
139     * @return
140     */
141    public <T> Set<T> getCacheSet(final String key)
142    {
143        return redisTemplate.opsForSet().members(key);
144    }
145
146    /**
147     * 缓存Map
148     *
149     * @param key
150     * @param dataMap
151     */
152    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
153    {
154        if (dataMap != null) {
155            redisTemplate.opsForHash().putAll(key, dataMap);
156        }
157    }
158
159    /**
160     * 获得缓存的Map
161     *
162     * @param key
163     * @return
164     */
165    public <T> Map<String, T> getCacheMap(final String key)
166    {
167        return redisTemplate.opsForHash().entries(key);
168    }
169
170    /**
171     * 往Hash中存入数据
172     *
173     * @param key Redis键
174     * @param hKey Hash键
175     * @param value 值
176     */
177    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
178    {
179        redisTemplate.opsForHash().put(key, hKey, value);
180    }
181
182    /**
183     * 获取Hash中的数据
184     *
185     * @param key Redis键
186     * @param hKey Hash键
187     * @return Hash中的对象
188     */
189    public <T> T getCacheMapValue(final String key, final String hKey)
190    {
191        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
192        return opsForHash.get(key, hKey);
193    }
194
195    /**
196     * 删除Hash中的数据
197     * 
198     * @param key
199     * @param hkey
200     */
201    public void delCacheMapValue(final String key, final String hkey)
202    {
203        HashOperations hashOperations = redisTemplate.opsForHash();
204        hashOperations.delete(key, hkey);
205    }
206
207    /**
208     * 获取多个Hash中的数据
209     *
210     * @param key Redis键
211     * @param hKeys Hash键集合
212     * @return Hash对象集合
213     */
214    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
215    {
216        return redisTemplate.opsForHash().multiGet(key, hKeys);
217    }
218
219    /**
220     * 获得缓存的基本对象列表
221     *
222     * @param pattern 字符串前缀
223     * @return 对象列表
224     */
225    public Collection<String> keys(final String pattern)
226    {
227        return redisTemplate.keys(pattern);
228    }
229}

2、登录认证

2.1修改登录接口

我们之前的案例已经写过登录接口相关的方法,现在我们需要修改这些方法使这些方法更规范一些

  • 修改UserDetailsServiceImpl实现类
 1        //根据用户名查询用户信息
 2        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
 3        wrapper.eq(User::getUserName,username);
 4        User user = userMapper.selectOne(wrapper);
 5        //如果查询不到数据就通过抛出异常来给出提示
 6        if(Objects.isNull(user)){
 7            throw new RuntimeException("用户名或密码错误");
 8        }
 9        //TODO 根据用户查询权限信息 添加到LoginUser中,后面会完善这些代码
10  
11        //封装成UserDetails对象返回 
12        return new LoginUser(user);
  • 还需要修改 登录接口相关的方法

LoginController

 1@RestController
 2public class LoginController {
 3
 4    @Autowired
 5    private LoginServcie loginServcie;
 6
 7    @PostMapping("/user/login")
 8    public ResponseResult login(@RequestBody User user){
 9        return loginServcie.login(user);
10    }
11}

LoginServiceImpl

 1@Service
 2public class LoginServiceImpl implements LoginServcie {
 3
 4    @Autowired
 5    private AuthenticationManager authenticationManager;
 6    @Autowired
 7    private RedisCache redisCache;
 8
 9    @Override
10    public ResponseResult login(User user) {
11  
12        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
13        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
14        if(Objects.isNull(authenticate)){
15            throw new RuntimeException("用户名或密码错误");
16        }
17        //使用userid生成token
18        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
19        String userId = loginUser.getUser().getId().toString();
20        String jwt = JwtUtil.createJWT(userId);
21        //authenticate存入redis
22        redisCache.setCacheObject("login:"+userId,loginUser);
23        //把token响应给前端
24        HashMap<String,String> map = new HashMap<>();
25        map.put("token",jwt);
26        return new ResponseResult(200,"登陆成功",map);
27    }
28}

上面这段代码就是我们通过用户名和密码登录后生成jwt并且将用户的信息以id未键的形式存入到redis中

  • 在SecurityConfig中添加下面的代码
1@Configuration
2public class SecurityConfig extends WebSecurityConfigurerAdapter {
3  @Bean
4    @Override
5    public AuthenticationManager authenticationManagerBean() throws Exception {
6        return super.authenticationManagerBean();
7    }
8}

在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。

2.2自定义认证过滤器

现在我们已经完成用户登录接口但是对于其他的接口访问却没有做好,需要我们自己定义一个过滤器,这个过滤器需要在请求发送时查看这些请求头是否携带了token,如果携带了token可以从请求头总取出然后经过jwt工具类获取用户id然后根据用户的id从redis中获取用户信息,存入到SecurityContextHolder,这里的SecurityContextHolder时security提供的上下文工具类,能在同一线程中传递用户信息。

  • 自定义认证过滤器
 1@Component
 2public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
 3
 4    @Autowired
 5    private RedisCache redisCache;
 6
 7    @Override
 8    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
 9        //获取token
10        String token = request.getHeader("token");
11        if (!StringUtils.hasText(token)) {
12            //放行
13            filterChain.doFilter(request, response);
14            return;
15        }
16        //解析token
17        String userid;
18        try {
19            Claims claims = JwtUtil.parseJWT(token);
20            userid = claims.getSubject();
21        } catch (Exception e) {
22            e.printStackTrace();
23            throw new RuntimeException("token非法");
24        }
25        //从redis中获取用户信息
26        String redisKey = "login:" + userid;
27        LoginUser loginUser = redisCache.getCacheObject(redisKey);
28        if(Objects.isNull(loginUser)){
29            throw new RuntimeException("用户未登录");
30        }
31        //存入SecurityContextHolder
32        //TODO 获取权限信息封装到Authentication中
33        UsernamePasswordAuthenticationToken authenticationToken =
34                new UsernamePasswordAuthenticationToken(loginUser,null,null);
35        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
36        //放行
37        filterChain.doFilter(request, response);
38    }
39}
  • 在配置文件中使用这个认证过滤器
 1  @Configuration
 2public class SecurityConfig extends WebSecurityConfigurerAdapter {
 3
 4
 5    @Bean
 6    public PasswordEncoder passwordEncoder(){
 7        return new BCryptPasswordEncoder();
 8    }
 9
10
11    @Autowired
12    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
13
14    @Override
15    protected void configure(HttpSecurity http) throws Exception {
16    http.formLogin();  
17    http
18                //关闭csrf
19                .csrf().disable()
20                //不通过Session获取SecurityContext
21                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
22                .and()
23                .authorizeRequests()
24                // 对于登录接口 允许匿名访问
25                .antMatchers("/user/login").anonymous()
26                // 除上面外的所有请求全部需要鉴权认证
27                .anyRequest().authenticated();
28
29        //把token校验过滤器添加到过滤器链中
30        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
31    }
32
33    @Bean
34    @Override
35    public AuthenticationManager authenticationManagerBean() throws Exception {
36        return super.authenticationManagerBean();
37    }
38}

2.3进行测试

  • 查看正常登录后是否会返回token
screenshot-58

当我们携带token访问其他接口时

3、授权

授权我们一般使用的是数据库中的权限信息,而在数据库中对与权限的管理也有专门的模型RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。

3.1导入相关数据

 1CREATE DATABASE /*!32312 IF NOT EXISTS*/`sg_security` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
 2
 3USE `sg_security`;
 4
 5/*Table structure for table `sys_menu` */
 6
 7DROP TABLE IF EXISTS `sys_menu`;
 8
 9CREATE TABLE `sys_menu` (
10  `id` bigint(20) NOT NULL AUTO_INCREMENT,
11  `menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
12  `path` varchar(200) DEFAULT NULL COMMENT '路由地址',
13  `component` varchar(255) DEFAULT NULL COMMENT '组件路径',
14  `visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
15  `status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
16  `perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
17  `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
18  `create_by` bigint(20) DEFAULT NULL,
19  `create_time` datetime DEFAULT NULL,
20  `update_by` bigint(20) DEFAULT NULL,
21  `update_time` datetime DEFAULT NULL,
22  `del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',
23  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
24  PRIMARY KEY (`id`)
25) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';
26
27/*Table structure for table `sys_role` */
28
29DROP TABLE IF EXISTS `sys_role`;
30
31CREATE TABLE `sys_role` (
32  `id` bigint(20) NOT NULL AUTO_INCREMENT,
33  `name` varchar(128) DEFAULT NULL,
34  `role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
35  `status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
36  `del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
37  `create_by` bigint(200) DEFAULT NULL,
38  `create_time` datetime DEFAULT NULL,
39  `update_by` bigint(200) DEFAULT NULL,
40  `update_time` datetime DEFAULT NULL,
41  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
42  PRIMARY KEY (`id`)
43) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
44
45/*Table structure for table `sys_role_menu` */
46
47DROP TABLE IF EXISTS `sys_role_menu`;
48
49CREATE TABLE `sys_role_menu` (
50  `role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
51  `menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',
52  PRIMARY KEY (`role_id`,`menu_id`)
53) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
54
55/*Table structure for table `sys_user` */
56
57DROP TABLE IF EXISTS `sys_user`;
58
59CREATE TABLE `sys_user` (
60  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
61  `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
62  `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
63  `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
64  `status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
65  `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
66  `phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
67  `sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
68  `avatar` varchar(128) DEFAULT NULL COMMENT '头像',
69  `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
70  `create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',
71  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
72  `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
73  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
74  `del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
75  PRIMARY KEY (`id`)
76) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
77
78/*Table structure for table `sys_user_role` */
79
80DROP TABLE IF EXISTS `sys_user_role`;
81
82CREATE TABLE `sys_user_role` (
83  `user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',
84  `role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',
85  PRIMARY KEY (`user_id`,`role_id`)
86) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这是根据用户id获取他的权限

 1SELECT 
 2		DISTINCT sm.`perms`
 3FROM 
 4	sys_user_role sur
 5	LEFT JOIN `sys_role` sr ON sur.`role_id`= sr.`id`
 6	LEFT JOIN `sys_role_menu` srm ON srm.`role_id`= sr.`id`
 7	LEFT JOIN `sys_menu` sm ON srm.`menu_id`= sm.`id`
 8
 9WHERE 
10	sur.`user_id`=1
11	AND sr.`status`= 0
12	AND sm.`status`= 0

菜单表(Menu)实体类

 1* 菜单表(Menu)实体类
 2 *
 3 * @author makejava
 4 * @since 2021-11-24 15:30:08
 5 */
 6@TableName(value="sys_menu")
 7@Data
 8@AllArgsConstructor
 9@NoArgsConstructor
10@JsonInclude(JsonInclude.Include.NON_NULL)
11public class Menu implements Serializable {
12    private static final long serialVersionUID = -54979041104113736L;
13  
14        @TableId
15    private Long id;
16    /**
17    * 菜单名
18    */
19    private String menuName;
20    /**
21    * 路由地址
22    */
23    private String path;
24    /**
25    * 组件路径
26    */
27    private String component;
28    /**
29    * 菜单状态(0显示 1隐藏)
30    */
31    private String visible;
32    /**
33    * 菜单状态(0正常 1停用)
34    */
35    private String status;
36    /**
37    * 权限标识
38    */
39    private String perms;
40    /**
41    * 菜单图标
42    */
43    private String icon;
44  
45    private Long createBy;
46  
47    private Date createTime;
48  
49    private Long updateBy;
50  
51    private Date updateTime;
52    /**
53    * 是否删除(0未删除 1已删除)
54    */
55    private Integer delFlag;
56    /**
57    * 备注
58    */
59    private String remark;
60}

3.2代码实现

我们只需要根据用户id去查询到其所对应的权限信息即可。

所以我们可以创建个MenuMapper接口,其中提供一个方法可以根据userid查询权限信息。然后在对应的xml中编写查询语句,在进行这项工作之前我们先在application.yml中配置mapperXML文件的位置

1  redis:
2    host: localhost
3    port: 6379
4mybatis-plus:
5  mapper-locations: classpath*:/mapper/**/*.xml 

测试能否正常查询用户的权限,在测试之前要导入测试相关的依赖并在UserMapper文件中编写对应的方法

1    <dependency>
2      <groupId>org.springframework.boot</groupId>
3      <artifactId>spring-boot-starter-test</artifactId>
4      <scope>test</scope>
5    </dependency>

UserMapper.java

1@Mapper
2public interface UserMapper extends BaseMapper<User> {
3    //通过用户id查询用户权限
4    List<String> selectPermsByUserId(Long id);
5}

UserMapper.xml

 1<?xml version="1.0" encoding="UTF-8" ?>
 2<!DOCTYPE mapper
 3        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 4        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 5<mapper namespace="com.sg.mapper.UserMapper">
 6    <select id="selectPermsByUserId" resultType="String">
 7        SELECT
 8        DISTINCT sm.`perms`
 9        FROM
10        sys_user_role sur
11        LEFT JOIN `sys_role` sr ON sur.`role_id`= sr.`id`
12        LEFT JOIN `sys_role_menu` srm ON srm.`role_id`= sr.`id`
13        LEFT JOIN `sys_menu` sm ON srm.`menu_id`= sm.`id`
14
15        <where>
16            sur.`user_id`=#{id}
17            AND sr.`status`= 0
18            AND sm.`status`= 0
19        </where>
20    </select>
21</mapper>

在测试类中编程测试代码

 1@SpringBootTest
 2public class TestObject {
 3
 4@Autowired
 5private UserMapper userMapper;
 6    @Test
 7    public void Test1() {
 8
 9        List<String> permissions =userMapper.selectPermsByUserId(1L);
10        System.out.println(permissions);
11    }
12}

出现这个说明我们的查询方法没有问题,我们现在已经查询到了用户的权限但是我们还没有把他放入到整个spring security的授权流程中,在这里我们只需要在把LoginUser中的getAuthorities这个方法修改一下。

 1package com.sg.domain;
 2
 3import com.alibaba.fastjson.annotation.JSONField;
 4import lombok.Data;
 5import lombok.NoArgsConstructor;
 6import org.springframework.security.core.GrantedAuthority;
 7import org.springframework.security.core.authority.SimpleGrantedAuthority;
 8import org.springframework.security.core.userdetails.UserDetails;
 9
10import java.util.Collection;
11import java.util.List;
12import java.util.stream.Collectors;
13
14/**
15 * ClassName: LoginUser
16 * Package: com.sg.domain
17 * Description:
18 *
19 * @Author hjh
20 * @Create 2024/10/28 8:41
21 * @Version 1.0
22 */
23@Data
24@NoArgsConstructor
25public class LoginUser implements UserDetails {
26    private User user;
27    private List<String> permissions;
28
29    public LoginUser(User user,List<String> permissions) {
30        this.user = user;
31        this.permissions = permissions;
32    }
33    //存储SpringSecurity所需要的权限信息的集合
34    /*
35   权限GrantedAuthority集合没法序列化,每次从redis获取那个权限集合都是空,每次调用获取权限的方法,都需要重新包装对象里面的权限集合,所以使用注解不进行序列化
36    */
37    @JSONField(serialize = false)
38    private List<GrantedAuthority> authorities;
39
40    @Override
41    public Collection<? extends GrantedAuthority> getAuthorities() {
42        if(authorities!=null){
43            return authorities;
44        }
45        //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
46        authorities = permissions.stream().
47                map(SimpleGrantedAuthority::new)
48                .collect(Collectors.toList());
49        return authorities;
50    }
51
52    @Override
53    public String getPassword() {
54        return user.getPassword();
55    }
56
57    @Override
58    public String getUsername() {
59        return user.getUserName();
60    }
61
62    @Override
63    public boolean isAccountNonExpired() {
64        return true;
65    }
66
67    @Override
68    public boolean isAccountNonLocked() {
69        return true;
70    }
71
72    @Override
73    public boolean isCredentialsNonExpired() {
74        return true;
75    }
76
77    @Override
78    public boolean isEnabled() {
79        return true;
80    }
81}

这里直接用构造函数将权限信息封装到当前类中,然后我们还需要去完成之前用户UserDetailsServiceImpl类中未完成的用户权限获取方法。

 1@Component
 2public class UserDetailsServiceImpl implements UserDetailsService {
 3    @Autowired
 4    private UserMapper userMapper;
 5
 6    @Override
 7    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
 8        //根据用户名查询用户信息
 9        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
10        wrapper.eq(User::getUserName, username);
11        User user = userMapper.selectOne(wrapper);
12        //如果查询不到数据就通过抛出异常来给出提示
13        if (Objects.isNull(user)) {
14            throw new RuntimeException("用户名或密码错误");
15        }
16        //根据用户查询权限信息 添加到LoginUser中
17        List<String> permissionKeyList = userMapper.selectPermsByUserId(user.getId());
18        //封装成UserDetails对象返回
19        return new LoginUser(user, permissionKeyList);
20    }
21}

这里完成后在登录校验中我们还需要把用户的权限信息存入到Authentication中,在JwtAuthenticationTokenFilter中完成。

 1@Component
 2public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
 3
 4    @Autowired
 5    private RedisCache redisCache;
 6
 7    @Override
 8    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
 9        //获取token
10        String token = request.getHeader("token");
11        if (!StringUtils.hasText(token)) {
12            //放行
13            filterChain.doFilter(request, response);
14            return;
15        }
16        //解析token
17        String userid;
18        try {
19            Claims claims = JwtUtil.parseJWT(token);
20            userid = claims.getSubject();
21        } catch (Exception e) {
22            e.printStackTrace();
23            throw new RuntimeException("token非法");
24        }
25        //从redis中获取用户信息
26        String redisKey = "login:" + userid;
27        LoginUser loginUser = redisCache.getCacheObject(redisKey);
28        if(Objects.isNull(loginUser)){
29            throw new RuntimeException("用户未登录");
30        }
31        //存入SecurityContextHolder
32        //TODO 获取权限信息封装到Authentication中
33        UsernamePasswordAuthenticationToken authenticationToken =
34                new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
35        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
36        //放行
37        filterChain.doFilter(request, response);
38    }

这里我们基本完成了一大半现在还剩最后的使用注解来标识,每个方法的访问权限。

在spring security的配置文件中添加注解,开启相关配置。

@EnableGlobalMethodSecurity(prePostEnabled = true)

在我们需要权限效验的方法上添加@PreAuthorize()来配置权限。

1@RestController
2public class HelloController {
3
4    @RequestMapping("/hello")
5    @PreAuthorize("hasAuthority('test')")
6    public String hello(){
7        return "hello";
8    }
9}

当我们在去访问时就需要查询是否有相应权限,有才会放行没有就会报错