3 授权
3.0 权限系统的作用
例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。
总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。
我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。实际上前端的校验防君子不防小人。
所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须基于所需权限才能进行相应的操作。
3.1 授权的基本流程
在SpringSecurity中, 会使用默认的FilterSecuritylnterceptor来进行权限校验。 在FilterSecuritylnterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
然后设置我们的资源所需要的权限即可。
这部分的工作实际上就是完善前面 UserDetailsServiceImpl 和 JwtAuthenticationTokenFileter 中的TODO。
3.2 授权实现
3.2.1 限制访问资源所需权限
SpringSecurity为我们提供了基于注解和基于配置两种权限控制方案。这我们项目中主要采用的方式是基于注解的。因为使用配置的方式往往是配置静态资源的,前后端分离项目很少使用,所以我们可以使用注解去指定访问对应的资源所需的权限。
在前面部分的代码中,我们会把权限信息写死,实际上权限应该是从数据库中拿到的。
但是要使用它,我们需要先开启相关配置(在SecurityConfig配置类中)。
@EnableGlobalMethodSecurity(prePostEnabled =true)
然后就可以使用对应的 @PreAuthorize 注解。示例如下:
@RestControllerpublicclassHellocontroller{@RequestMapping("/hello")@PreAuthorize("hasAuthority('test')")publicStringhel1o(){return"hello";}}
3.2.2封装权限信息
我们前面在写UserDetailsServicelmpl的时候说过,在查询出用户后还要获取对应的权限信息,封装到UserDetails中返回。
我们先直接把权限信息写死封装到UserDetails中进行测试。
之前定义了UserDetails的实现类LoginUser,想要让其能封装权限信息就要对其进行修改。
UsernamePasswordAuthenticationToken方法所使用的三个形参及其含义如下表所示。
形参名含义loginUser用户名credentials密码authorities权限信息
修改后的LoginUser类:
packagedomain;import.../**
* UserDetails的实现类
*/@Data@NoArgsConstructorpublicclassLoginUserimplementsUserDetails{privateUser user;privateList<String> permissions;publicLoginUser(User user,List<String> permissions){this.user = user;this. permissions = permissions;}//重写方法@JSONField(serialize =false)privateList<SimpleGrantedAuthority> authorities;@OverridepublicCollection<?extendsGrantedAuthority>getAuthorities(){//把 permissions 中的String类型的权限信息封装成SimpleGrantedAuthority对象//两种写法,一种是使用for循环遍历集合,一种是使用stream流,这里两种写法都给出,但推荐使用Stream流的写法if(authorities !=null){return authorities;}/**
* //写法一
* newList = new ArrayList<>();
* for (String permission: permissions){
* SimpleGrantedAuthority authority = new SimpleGrantedAuthority (permission);
* newList.add(authority);
* }
*///方法二
authorities = permissions.stream().map (Simp leGrantedAuthority::new).collect(Collectors.toList());return authorities;}@OverridepublicStringgetPassword(){return user.getPassword();}@OverridepublicStringgetUsername(){return user.getUserName();}@OverridepublicbooleanisAccountNonExpired(){returntrue;}@OverridepublicbooleanisAccountNonLocked(){returntrue;}@OverridepublicbooleanisCredentialsNonExpired(){returntrue;}@OverridepublicbooleanisEnabled(){returntrue;}}
在上面的代码中,我们可以看到一个泛型 SimpleGrantedAuthority ,它是由Spring提供的,但是我们在存储进redis中的时候,为了安全考虑,默认情况下是不会把SimpleGrantedAuthority进行序列化存入的,如果不做操作的话,java会报异常。
解决的方案就是将 authorities 不存入redis当中,只用把 permissions 序列化存入即可。这里我们对它进行一个忽略,由 @JSONField(serialize = false) 实现。
如果加了注解还是报错: default constructor not found。可以把fastjson的版本改成1.2.49。
修改后的UserDetailsServiceImpl:
packageservice.impl;import...@ServicepublicclassUserDetailsServiceImplimplementsUserDetailsService{@AutowiredprivateUserMapper userMapper;@OverridepublicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{privateUserMapper//查询用户信息LambdaQueryWrapper<User> queryWeapper=newLambdaQueryWrapper();
queryWrapper.eq(User::getUserName,username);User user = userMapper.selectOne(queryWrapper);//如果没有查询到用户,就抛出异常if(Object.isNull(user)){thrownewRuntimeException("用户名或密码错误");}//TODO 查询对应的权限信息,此处这么写是为了方便测试List<String> list =newArrayList<>(Arrays.asList("test","admin"));//把数据封装成UserDetails返回returnnewLoginUser(user, list);}}
上面是登陆的方法要为增加权限做的修改。
我们的代码中还有一个 TODO ,是在 JwtAuthenticationTokenFilter 认证过滤器中。
packagefilter;import...publicclassJwtAuthenticationTokenFilterextendsOncePerRequestFilter{@AutowiredprivateRedisCache redisCache;@OverrideprotectedvoiddoFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain
filterChain)throwsServletException,IOException{//直接放行不等于不设置SecurityContextHolder,不设置SecurityContextHolder就没法通过认证到达Api,会被后面的filter给拦住//获取请求头中的tokenString token = request.getHeader("token");if(!Stringutils.hasText(token)){//放行
filterchain.doFilter(request,response);//过滤器中doFilter方法前面的逻辑是请求进来时执行的内容,doFilter后面的逻辑是响应时执行的内容,直接return了,响应时就不会执行后面的内容了return;}//解析token获取userid
string userId;try{Claims claims =JwtUtil.parseJWT(token);
userId = claims.getsubject();}catch(Exception e){
e.printStackTrace();thrownewRuntimeException("token非法");}//通过 userId 从redis中获取用户信息String redisKey ="login:"+userId;LoginUser loginUser = redisCache.getCacheObject(redisKey);if(Object.isNull(loginUser)){thrownewRuntimeException("用户未登录");}//如果从redis中获取到loginUser,就存入SecurityContextHolder//TODO 获取权限信息封装到Authentication中//前面登录时用两参,对认证状态还未确认,之后调用ProviderManager对账号密码进行确认后,返回的那个Authentication是认证的。UsernamePasswordAuthenticationToken authenticationToken =newUsernamePasswordAuthenticationToken(loginUser,null, loginUser.getAuthorities);SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行
filterchain.doFilter(request,response);}}
版权归原作者 悠玄烛远 所有, 如有侵权,请联系我们删除。