SpringSecurity实现前后端分离登录授权详解

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 在介绍完SpringSecurity实现前后端分离认证之后,然后就是SpringSecurity授权,在阅读本文章之前可以先了解一下作者的上一篇文章SpringSecurity认证SpringSecurity实现前后端分离登录token认证详解_山河亦问安的博客-CSDN博客。

1. 授权


1.1 权限系统的作用


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


总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。


我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可)不通过前端,直接去发送请求来实现相关功能操作。


所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须基于所需权限才能进行相应的操作。



1.2 授权基本流程


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


1.3 授权实现


1.3.1 限制访问资源所需权限


SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。  但是要使用它我们需要先开启相关配置。我们需要在springSecurity配置类上加下面这行代码:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
}

@EnableGlobalMethodSecurity 注解参数说明:


prePostEnabled = true 会解锁 @PreAuthorize 和 @PostAuthorize 两个注解。顾名思义,@PreAuthorize 注解会在方法执行前进行验证,而 @PostAuthorize 注解会在方法执行后进行验证。

securedEnabled = true 会解锁 @Secured 注解。@Secured 是专门用于判断是否具有角色的。能写在方法或类上。参数要以 ROLE_开头。


1.3.2 权限校验方法


hasAuthority(String)


判断用户是否具有特定的权限

    @PostMapping("/test1")
    @PreAuthorize("hasAuthority('test1')")
    public String test1(){
       return "test";
    }


以上面代码为例,按Ctrl+Alt,然后点击上面的hasAuthority进入源码打断点如下图:


利用Apipost发送请求得到的结果如下图:


代码分析:

private boolean hasAnyAuthorityName(String prefix, String... roles) {
        Set<String> roleSet = this.getAuthoritySet(); //从SecurityContextHolder.getContext()中获取用户的权限集合
        String[] var4 = roles; //这是我们测试方法中hasAuthority中的权限信息test1
        int var5 = roles.length;
        for(int var6 = 0; var6 < var5; ++var6) {
            String role = var4[var6];
            String defaultedRole = getRoleWithDefaultPrefix(prefix, role); //遍历var4数组,其中的每个权限信息比如test1加上前缀,比如pre+test1,这里pre为null,如果roleset中包含这个权限信息就会返回true,该方法就会正常进行(roleSet.contains(defaultedRole)) {
                return true;
            }
        }
        return false;
    }

hasAnyAuthority(String …)

hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。

    @PostMapping("/test")
    @PreAuthorize("hasAuthority('test1','test')")
    public String tes1t(){
       return "test";
    }

hasRole(String)

hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

   @PreAuthorize("hasRole('test')")
    public String hello(){
        return "hello";
    }


hasAnyAuthority(String …)


hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。


1.3.3 自定义权限校验方法

我们也可以定义自己的权限校验方法,在@PreAuthorize注解中使用我们的方法。代码如下:

@Component("ex")
public class SGExpressionRoot {
    public boolean hasAuthority(String authority){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        MyUser myUser = (MyUser) authentication.getPrincipal();
        List<String> permissions = myUser.getTbUser().getPermissions();
        return permissions.contains(authority);
    }
}

SPEL表达式中使用 @ex相当于获取容器中bean对象。然后再调用这个对象的hasAuthority方法,代码如下:

    @PostMapping("/test")
    @PreAuthorize("@ex.hasAuthority('test')")
    public TbUser test(){
        TbUser tbUser = tbUserMapper.selectById(1);
        return tbUser;
    }


1.4 自定义失败方案


我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。 在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。


如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。  代码如下:

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        Result result = Result.error("401", "用户认证失败,请重新登录");
        response.setStatus(200);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().print(result.toString());
    }
}


如果是授权过程中出现的异常会被封装成AccessDeniedException,然后调用AccessDeniedHandler对象的方法去进行异常处理。 代码如下:

@Component
public class AccessDenieHandleImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Result result = Result.error("403", "用户权限不足");
        response.setStatus(200);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().print(result.toString());
    }
}

 

所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPointAccessDeniedHandler然后配置给SpringSecurity即可。配置代码如下:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private AccessDenieHandleImpl accessDenieHandle;
    @Autowired
    private AuthenticationEntryPointImpl authenticationEntryPoint;
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) //配置认证失败处理器
                    .accessDeniedHandler(accessDenieHandle); //配置授权失败处理器
      http.cors() //允许跨域
    }
}


1.5 代码更改


在认证的基础上添加授权,为了方便这里的权限信息设计写死,在真正的项目中每个用户的权限应该从数据库中进行查询。代码设计如下:

 

MyUser类代码如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class MyUser implements UserDetails, Serializable {
    private TbUser tbuser;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> collect = tbuser.getPermissions().stream().map(SimpleGrantedAuthority::new).distinct().collect(Collectors.toList());
        return collect;
    }
    @Override
    public String getPassword() {
        return sysUser.getPassword();
    }
    @Override
    public String getUsername() {
        return sysUser.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;
    }
}

JwtAuthenticationTokenFilter类代码如下:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    RedisTemplate redisTemplate;
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        //获取请求头中的token
        String token = httpServletRequest.getHeader("token");
        //如果token为空直接放行,由于用户信息没有存放在SecurityContextHolder.getContext()中所以后面的过滤器依旧认证失败符合要求
        if(!StringUtils.hasText(token)){
            filterChain.doFilter(httpServletRequest,httpServletResponse);
            return;
        }
        Long userId;
        try {
            //通过jwt工具类解析token获得userId,如果token过期或非法就会抛异常
            DecodedJWT decodedJWT = JwtUtil.decodeToken(token);
            userId = decodedJWT.getClaim("userId").asLong();
        }catch (Exception e){
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //根据userId从redis中获取用户信息,如果没有该用户就代表该用户没有登录过
        TbUser myUser = (TbUser) redisTemplate.opsForValue().get(String.valueOf(userId));
        if(Objects.isNull(myUser)){
            throw new RuntimeException("用户未登录");
        }
        MyUser myUser1=new MyUser(myUser);
        //将用户信息存放在SecurityContextHolder.getContext(),后面的过滤器就可以获得用户信息了。
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(myUser1,null,myUser1.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}

UserDetailServiceImpl

@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    TbUserMapper tbUserMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<TbUser> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("username",username);
        TbUser tbUser = tbUserMapper.selectOne(queryWrapper);
        List<String> list = Arrays.asList("test");
        tbUser.setPermissions(list);
        if(tbUser==null){
            throw new RuntimeException("用户名或者密码错误");
        }
        return new MyUser(tbUser);
    }
}

至此SpringSecurity实现前后端分离token验证认证授权就此结束。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
5月前
|
安全 Java 数据库
SpringSecurity+JWT前后端分离架构登录认证
在SpringSecurity实现前后端分离登录token认证详解_springsecurity前后端分离登录认证-CSDN博客基础上进行重构,实现前后端分离架构登录认证,基本思想相同,借鉴开源Gitee代码进行改造,具有更好的代码规范。
303 1
|
缓存 安全 NoSQL
SpringSecurity实现前后端分离登录token认证详解
SpringSecurity实现前后端分离登录token认证详解
658 1
|
JSON 前端开发 数据格式
六.SpringSecurity基础-认证授权结果处理
SpringSecurity基础-认证授权结果处理
|
JSON 前端开发 数据格式
SpringSecurity基础-认证授权结果处理
在传统的应用中,认证成功后页面需要跳转到认证成功页面或者跳转到个人中心页,但是在前后端分离的项目通常是使用Ajax请求完成认证,这时候我们需要返回一个JSON结果告知前端认证结果,然后前端自行跳转页面。 要做到上述功能,我们需要自定义认证成功处理器实现AuthenticationSuccessHandler接口复写 onAuthenticationSuccess方法,该方法其中一个参数是Authentication ,他里面封装了认证信息,用户信息UserDetails等,我们需要在这个方法中使用Response写出json数据即可
125 0
|
安全 API 数据库
SpringSecurity基础-授权流程
授权一定是在认证通过之后,授权流程是通过FilterSecurityInterceptor拦截器来完成,FilterSecurityInterceptor通过调用SecurityMetadataSource来获取当前访问的资源所需要的权限,然后通过调用AccessDecisionManager投票决定当前用户是否有权限访问当前资源。授权流程如下
124 0
|
安全 前端开发 Java
Spring Security5+ 用户认证、授权及注销
Spring Security5+ 用户认证、授权及注销
271 0
|
数据库 数据安全/隐私保护
8、SpringBoot2.0整合Shiro实现登录认证和权限管理(八)
添加相关的依赖,spring-boot-starter-data-jpa在 IEDA中创建SpringBoot2.0项目-超详细(一)博客中已经添加
142 0
8、SpringBoot2.0整合Shiro实现登录认证和权限管理(八)
|
移动开发 安全 前端开发
SpringSecurity认证和授权
目前,我们的测试环境,是谁都可以访问的,我们使用 Spring Security 增加上认证和授权的功能
SpringSecurity认证和授权
|
JSON 算法 安全
【SpringBoot技术指南】「权限校验专区」Shiro整合JWT授权和认证实现
【SpringBoot技术指南】「权限校验专区」Shiro整合JWT授权和认证实现
313 0
|
安全 数据库
SpringSecurity(安全框架)用户认证和授权
SpringSecurity(安全框架)用户认证和授权
SpringSecurity(安全框架)用户认证和授权