Spring Security 授权

Spring Security 用户授权

        本篇文章主要学习SpringSecurity 的授权功能实现,如果对登录认证还不太清楚的小伙伴们可去观看之前的一篇文章  SpringSecurity 的登录认证,链接地址:Spring Security 登录认证-CSDN博客

 1.引言

1-1.权限的作用

        例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息、删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。

        用一句话说就是:不同的角色有不同的权限,对应着不同的功能。这就是权限要去实现的效果。​ 所以我们需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。

1-2.授权流程

        在 SpringSecurity 中,会使用默认的 FilterSecurityInterceptor 来进行权限校验。它会从 SecurityContextHolder 获取其中的 Authentication,然后获取到其中的权限信息。所以我们在项目中只需要把当前登录用户的权限信息也存入 Authentication。然后设置我们的资源所需要的权限即可。

2.开始授权

        在 SpringSecurity 中,为我们提供了注解的权限控制方发,本篇文章也是通过注解的方式对受限资源进行权限控制。

2-1.开启授权注解功能

找到我们的 SecurityConfig 类,在类上使用 @EnableGlobalMethodSecurity 注解

2-2.限制访问路径接口权限

        开启权限注解后,我们就可以开始使用他的一些权限注解:

2-2-1.@Secured
    /*
    * @Secured :校验登录用户是否有需要的角色
    * 属性value:String[] 类型,相当于hasAnyRole
    * 注解不能自动拼接角色前缀,需要手动加上去
    * @Secured("ROLE_admin"):只能是admin角色可以访问
    * @Secured({"ROLE_admin","ROLE_user"}):admin 和 user 角色都可以访问
    * */
    //@Secured("ROLE_admin")
    @Secured({"ROLE_admin","ROLE_user"})
    @GetMapping("/test1")
    public String test1(){
        return "Hello Security (test1)";
    }

        注意:如果我们要求,只有同时拥有admin & user的用户才能方法test()方法,这时候@Secured就无能为力了。

        如果 @Secured 无效则:在 @EnableGlobalMethodSecurity 注解上加入 securedEnabled = true

        

@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
2-2-2.@PreAuthorize
    /*
    * @PreAuthorize: 方法执行前,校验权限是否满足,方法相当于access
    * 属性value - 字符串 提供access方法动态表达式,如hasRole("ROLE_xxx") hasAuthority("xxx")
    * @PreAuthorize("hasRole('admin')"):只用拥有admin角色才可以访问
    * @PreAuthorize("hasAnyRole('admin','user')"):只要拥有 admin 和 user 其中一种角色就可以访问
    * @PreAuthorize("hasRole('admin') and hasRole('user')"):同时拥有 admin 和 user 才可以访问
    * */
    //@PreAuthorize("hasRole('admin')")
    //@PreAuthorize("hasRole('admin') and hasRole('user')")
    @PreAuthorize("hasAnyRole('admin','user')")
    @GetMapping("/test2")
    public String test2(){
        return "Hello Security (test2)";
    }

说明:该注解会在方法执行前校验权限

注意:@PreAuthorize("hasAnyRole('admin','user')"):

        这里小编使用的是 hasAnyRole 方法,这个需要在向 SecurityContextHolder 中设置权限的时候要加前缀 ROLE_ 。

        还有一个是 hasAnyAuthority 是不需要加前缀的,加了前缀也是会失效的

2-2-3.@PostAuthorize
    /*
    * @PostAuthorize: 方法执行前后,校验权限是否满足
    * 用法: 与上面 @PreAuthorize() 一致
    * */
    //@PostAuthorize("hasRole('admin')")
    //@PostAuthorize("hasRole('admin') and hasRole('user')")
    @PostAuthorize("hasAnyRole('admin','user')")
    @GetMapping("/test3")
    public String test3(){
        return "Hello Security (test3)";
    }

        说明:该注解是在方法执行后进行权限校验

2-3.自定义失败处理

        我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。

         在 SpringSecurity 中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter 捕获到。它会去判断是 认证失败 还是 授权失败 导致出现的异常。

        认证失败:封装成 AuthenticationException 然后调用 AuthenticationEntryPoint 对象的方法去进行异常处理。

        授权失败:封装成 AccessDeniedException 然后调用 AccessDeniedHandler 对象的方法去进行异常处理。

        因此我们要自定义异常处理,我们需要定义 AuthenticationException 和 AccessDeniedException 然后将他们配置在 SpringSecurity 中即可。

2-3-1.登录异常
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    
    /*
     * 认证失败 处理
     * */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        Map<String, Object> map = new HashMap<>();
        map.put("code",501);
        map.put("msg","登录认证失败!");

        response.setContentType("application/json;charset=UTF-8");
        String s = JSON.toJSONString(map);
        response.getWriter().println(s);
    }
}
2-3-2.授权异常
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Map<String, Object> map = new HashMap<>();
        map.put("code",502);
        map.put("msg","权限不足!");

        response.setContentType("application/json;charset=UTF-8");
        String s = JSON.toJSONString(map);
        response.getWriter().println(s);
    }
}

2-4.修改代码

        修改代码前需要创建数据库,和代码的实体类等,这里就不过多介绍了,主要创建的表为:sys_user :用户信息表,sys_user_role :用户角色中间表,sys_role : 角色信息表,sys_role_menu : 角色菜单中间表,sys_menu :菜单表

2-4-1.SecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)//开启授权注解
public class SecurityConfig extends WebSecurityConfigurerAdapter implements WebMvcConfigurer {

    @Resource
    private AuthenticationTokenFilter authenticationTokenFilter;
    @Resource
    private AuthenticationEntryPointImpl authenticationEntryPoint;
    @Resource
    private AccessDeniedHandlerImpl accessDeniedHandler;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //过滤请求
                .authorizeRequests()
                // 静态资源或者放行接口 允许匿名访问
                .antMatchers("/login/user_login").permitAll()
                //options 方法的请求放行
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

        //把token校验过滤器添加到过滤器链中
        http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //允许跨域(开启跨域)
        http.cors();

        //自定义异常
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
    }
}
2-4-2.LoginUserDetails
@Data
public class LoginUserDetails implements UserDetails {

    private String token;
    private User user;
    //存放当前登录用户的权限信息,一个用户可以有多个权限
    private List<String> permissions;
    public LoginUserDetails(User user,List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    //权限集合
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities != null) {
            return authorities;
        }

        /*
         * 把 permissions 中 String类型的权限信息封装成SimpleGrantedAuthority(转换成 GrantedAuthority 对象存入 authorities 中)
         * */
        authorities = permissions.stream().map(r -> {
            return new SimpleGrantedAuthority(r);
        }).collect(Collectors.toList());

        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getLoginName();
    }

    //是否未过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //是否未锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    //凭证是否未过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //是否可用
    @Override
    public boolean isEnabled() {
        return true;
    }
}
2-4-3.UserDetailsServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private UserMapper userMapper;
    @Resource
    private RoleMapper roleMapper;
    @Resource
    private UserRoleMapper userRoleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //(认证,即校验该用户是否存在)查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getLoginName,username);
        User user = userMapper.selectOne(queryWrapper);

        //如果没有查询到用户
        if (Objects.isNull(user)){
            throw new RuntimeException("用户名或者密码错误");
        }

        // (授权,即查询用户具有哪些权限)查询对应的用户信息
        //查询用户角色中间表
        LambdaQueryWrapper<UserRole> userRoleLambdaQueryWrapper = new LambdaQueryWrapper<>();
        userRoleLambdaQueryWrapper.eq(UserRole::getUserId,user.getId());
        List<UserRole> userRoles = userRoleMapper.selectList(userRoleLambdaQueryWrapper);
        //将 role_id 存入集合
        List<Integer> roleIds = userRoles.stream().map(ur -> ur.getRoleId()).collect(Collectors.toList());
        //查询 角色信息表
        LambdaQueryWrapper<Role> roleLambdaQueryWrapper = new LambdaQueryWrapper<>();
        roleLambdaQueryWrapper.in(Role::getId,roleIds);
        List<Role> roles = roleMapper.selectList(roleLambdaQueryWrapper);
        List<String> roleNames = roles.stream().map(r -> r.getName()).collect(Collectors.toList());

        return new LoginUserDetails(user,roleNames);
    }
}
2-4-5.LoginServiceImpl 中的登录方法
@Override
    public Map<String,Object> user_login(LoginVo loginVo) {
        Map<String,Object> map = new HashMap<>();

        //通过UsernamePasswordAuthenticationToken获取用户名和密码
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getLoginName(), loginVo.getLoginPwd());
        //AuthenticationManager委托机制对authenticationToken 进行用户认证
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        //如果认证没有通过,给出对应的提示
        if (Objects.isNull(authenticate)) {
            map.put("err","用户名或密码错误");
        }

        //如果认证通过,使用user 通过 jwt 生成 token 返回token
        //拿到这个当前登录用户信息
        LoginUserDetails loginUserDetails = (LoginUserDetails) authenticate.getPrincipal();
        //获取当前用户的user
        User user = loginUserDetails.getUser();

        String token = JwtUtil.getToken(user.getId());

        //置空密码 : 我这边使用了保存明文密码,所以这边要隐藏明文密码,实际开发一般不选择保存明文密码
        user.setLoginPwd(null);

        loginUserDetails.setToken(token);

        //将完整的用户信息存入redis缓存
        redisTemplate.opsForValue().set(RedisUtils.LOGIN_USER_ID + user.getId(), loginUserDetails, RedisUtils.TOKEN_TIMES, TimeUnit.HOURS);

        map.put("code",200);
        map.put("msg","认证成功");
        map.put("token",token);

        return map;
    }

2-4-6.AuthenticationTokenFilter

@Slf4j
@Component
public class AuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisTemplate redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        //放行登录接口
        String requestURI = request.getRequestURI();
        if (requestURI.equals("/login/user_login")){
            filterChain.doFilter(request, response);
            return;
        }

        //获取token
        String token = request.getHeader("token");
        if (token == null || token.equals("")) {
            this.return_message(response, 501, "请登录!");
            return;
        } else {
            token = token.replace("Bearer ", "");
        }

        //解析token 获取userId
        Integer userId = null;
        try {
            userId = JwtUtil.getUserId(token);
        } catch (Exception e) {
            this.return_message(response, 501, "非法登录令牌!");
            return;
        }

        //从redis中获取用户信息
        LoginUserDetails user = (LoginUserDetails) redisTemplate.opsForValue().get(RedisUtils.LOGIN_USER_ID + userId);
        if (Objects.isNull(user)) {
            this.return_message(response, 501, "未登录!");
            return;
        }

        //封装Authentication对象存入SecurityContextHolder && 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null,user.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        //放行
        filterChain.doFilter(request, response);
    }

    /*
     * 返回信息
     * */
    public void return_message(HttpServletResponse response, Integer code, String msg) {
        Map<String,Object> map = new HashMap<>();
        map.put("code",code);
        map.put("msg",msg);
        String s = JSON.toJSONString(map);
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
        } catch (IOException e) {
            log.info("信息返回异常:{}",e);
        }
        writer.write(s);
    }

}

3.测试

4.异常信息

4-1.一直提示权限不足

        大家在测试的时候不知道有没有出现,我明明登录并且授权了,却一直提示权限不足的问题。这个问题就要给大家说一下 hasAnyRole 与 hasAnyAuthority 的区别了:

  • 首先两者的功能是一样的都是控制权限
  • hasAnyRole : 基于角色控制权限,这个是需要在吧权限放入 SecurityContextHolder 的时候要加前缀 “ROLE_” 。
  • hasAnyAuthority : 他是不需要前缀的,查询出来的权限菜单或角色等可以直接拿来使用。
4-1-1.方法一

        将 controller 层权限注解中的 hasAnyRole 换成 hasAnyAuthority ,重新运行登录即可。

4-1-2.方法二

        找到我们的 UserDetailsServiceImpl 类中的 loadUserByUsername 方法,在下面设置权限的时候加入前缀 "ROLE_"。

        注意:此时的 /test22 接口因为使用的是 hasAnyAuthority ,所以这个接口出现权限不足提示。

        本文对于授权功能的实现到此就结束了,小编仍在学习中,如有不足的地方,欢迎大家评论区留言一起学习进步。

  • 39
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值