【Spring Security】安全框架学习(八)

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 注解。示例如下:

@RestController
public class Hellocontroller {
	
    @RequestMapping("/hello")
	@PreAuthorize("hasAuthority('test')")
	public String hel1o(){
		return "hello";
    }
}

3.2.2封装权限信息

我们前面在写UserDetailsServicelmpl的时候说过,在查询出用户后还要获取对应的权限信息,封装到UserDetails中返回。

我们先直接把权限信息写死封装到UserDetails中进行测试。

之前定义了UserDetails的实现类LoginUser,想要让其能封装权限信息就要对其进行修改。

UsernamePasswordAuthenticationToken方法所使用的三个形参及其含义如下表所示。

形参名含义
loginUser用户名
credentials密码
authorities权限信息

修改后的LoginUser类:

package domain;

import ...
    
/**
 * UserDetails的实现类
 */
    
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
    
    private User user;
    
    private List<String> permissions;
    
    


    public LoginUser(User user, List<String> permissions){
		this.user = user;
		this. permissions = permissions;
    }

    //重写方法
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;
    
    @Override
	public Collection<? extends GrantedAuthority> 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;
        
    }
    
	@Override
	public String getPassword() {
		return user.getPassword();
    }

	@Override
	public String getUsername() {
		return user.getUserName();
    }
    
	@Override
	public boolean isAccountNonExpired(){
		return true;
    }
    
    @Override
	public boolean isAccountNonLocked(){
		return true;
    }
    
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
    }
	
    @Override
	public boolean isEnabled(){
		return true;
	}
}

在上面的代码中,我们可以看到一个泛型 SimpleGrantedAuthority ,它是由Spring提供的,但是我们在存储进redis中的时候,为了安全考虑,默认情况下是不会把SimpleGrantedAuthority进行序列化存入的,如果不做操作的话,java会报异常。

解决的方案就是将 authorities 不存入redis当中,只用把 permissions 序列化存入即可。这里我们对它进行一个忽略,由 @JSONField(serialize = false) 实现

如果加了注解还是报错: default constructor not found。可以把fastjson的版本改成1.2.49。

修改后的UserDetailsServiceImpl

package service.impl;

import ...

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
	
    @Autowired
    private UserMapper userMapper;
    
    @Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        private UserMapper 
        
        //查询用户信息
        LambdaQueryWrapper<User> queryWeapper=new LambdaQueryWrapper();
        queryWrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(queryWrapper);
        
        //如果没有查询到用户,就抛出异常
        if(Object.isNull(user)) {
            throw new RuntimeException("用户名或密码错误");
        }
        
  		//TODO 查询对应的权限信息,此处这么写是为了方便测试
		List<String> list = new ArrayList<>(Arrays.asList("test","admin"));
        
        //把数据封装成UserDetails返回
        return new LoginUser(user, list);
	}
}

上面是登陆的方法要为增加权限做的修改。

我们的代码中还有一个 TODO ,是在 JwtAuthenticationTokenFilter 认证过滤器中。

package filter;

import ...

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    
    @Autowired
	private RedisCache redisCache;
    
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain
filterChain) throws ServletException, IOException {
		
        //直接放行不等于不设置SecurityContextHolder,不设置SecurityContextHolder就没法通过认证到达Api,会被后面的filter给拦住
        
        //获取请求头中的token
		String 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();
            throw new RuntimeException("token非法");
        }
        
        //通过 userId 从redis中获取用户信息
        String redisKey = "login:"+userId;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Object.isNull(loginUser)) {
            throw new RuntimeException("用户未登录");
        }
        
        //如果从redis中获取到loginUser,就存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        //前面登录时用两参,对认证状态还未确认,之后调用ProviderManager对账号密码进行确认后,返回的那个Authentication是认证的。
       UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities); 
       SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterchain.doFilter(request,response);
    }

}
end
  • 作者:dicraft(联系作者)
  • 更新时间:2022-09-02 09:29
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转载栈主转载的文章,请附上原文链接
  • 评论

    新增邮件回复功能,回复将会通过邮件形式提醒,请填写有效的邮件!