SpringBoot+Mybatis+PostMan(十一):用户角色权限访问控制三(将对象user/admin/superAdmin中token、authority、privilegeList进行封


在最后的检查阶段,果然还是发现项目存在一系列的问题,其中最主要的是,之前项目中redis存储数据时候,token是一个key,token值是一个value;authorities是一个key,对应角色又是一个value,而且登陆用户对应的资源在用户登陆的时候就要获取到,进而存储到redis中,这样做的目的是可以减轻后面访问其他接口的负担。这样一来,redis中key就变成了又3个key;而且这只是一个用户登陆时候存储在redis中信息,如果是很多个用户在登陆注册,可想而知,redis会变得十分的混乱,因此我们这里要进行优化,将该用户类对应的属性信息全部包装成一个value,key就是用户的登陆名,这样一来,当从redis中查询用户信息的时候,根据用户key就都能获取到。

同样也是在之前的项目上继续复写。项目代码地址 :https://github.com/yeyuting-1314/roleControll-3.0-redisAndPrivilege-3.0-redisTemplateAndJwt.git

首先来做一个准备工作。

一、准备工作

1 redis配置   在application.properties中配置redis信息:

#redis配置
spring.redis.host=127.0.0.1
spring.redis.port=6379

2. TokenUtil类实现token加密和解密,这里要实现jwt解密的原因是在访问其他接口的时候我们能获取到的只有接口头信息的token,如果我们要获取redis中key的话就需要对token进行解密,将对应的用户名解密出来(也就是redis的key),进而能顺利访问redis。

public String getToken(User user) {
        String token = null;
        try {
            Date expiresAt = new Date(System.currentTimeMillis() + 24L * 60L * 3600L * 1000L);
            token = JWT.create()
                    .withIssuer("auth0")
                    .withClaim("username", user.getUserName())
                    .withClaim("password", user.getPassword())
                    .withExpiresAt(expiresAt)
                    // 使用了HMAC256加密算法。
                    // mysecret是用来加密数字签名的密钥。
                    .sign(Algorithm.HMAC256("mysecret"));
        } catch (JWTCreationException exception){
            //Invalid Signing configuration / Couldn't convert Claims.
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return token;
    }
public DecodedJWT deToken(final String token) {
        DecodedJWT jwt = null;
        try {
            // 使用了HMAC256加密算法。
            // mysecret是用来加密数字签名的密钥。
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256("mysecret"))
                    .withIssuer("auth0")
                    .build(); //Reusable verifier instance
            jwt = verifier.verify(token);
        } catch (JWTVerificationException exception) {
            //Invalid signature/claims
            exception.printStackTrace();
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return jwt;
    }

二、第一阶段  用户登陆验证生成token信息

1. 首先是对UserServceImpl类中loadUserByUsername方法进行修改。

(1)根据用户登陆名判断数据库中是否存在此用户信息,如果不存在则返回

(2)如果存在则进行下面逻辑,拿到该用户在数据库中对应的角色,并将角色存储于grantedAuthority中,grantedAuthority作用是存储角色信息,后面返回的时候将角色信息带到用户角色认证类AccessDecisionManager中进行对比。

(3)获取到对应的token和角色以及资源后都封装到User类中,接着将User类装载到redis中。

将对象塞进redis:

valueOperations.set(user.getUserName() , user);

 @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List grantedAuthorities = new ArrayList<>() ;
        //User user = userMapper.selectByName(username) ;
        User user = userMapper.selectPrivilegesByUserName(username) ;
        if(user == null){
            throw new UsernameNotFoundException("用户不存在!")  ;
        }
        List userRoles = userRoleMapper.selectList(user.getId()) ;
        for(UserRole userRole : userRoles){
            Role role = roleMapper.selectOne(userRole.getRoleId()) ;
            GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRole()) ;
            grantedAuthorities.add(grantedAuthority) ;
        }

        //将原有的token值全部干掉,防止重复登陆
        //存入键值对
        ValueOperations valueOperations = redisTemplate.opsForValue() ;
        User newUser = (User)valueOperations.get(user.getUserName()) ;
        String redisToken = newUser.getToken() ;
        if(redisToken != null ){
            jedisUtil.delString(user.getUserName());
        }
        //生成token
        String token = tokenUtil.getToken(user) ;
        //token和权限存入user对象
        user.setToken(token);
        user.setGrantedAuthorities(grantedAuthorities);
        //对象存入redis
        valueOperations.set(user.getUserName() , user);

        return new org.springframework.security.
                core.userdetails.User("user" , new BCryptPasswordEncoder().encode(user.getPassword()) , grantedAuthorities) ;

    }

2. 执行完毕后进入到CustomAuthenticationSuccessHandler类,这里我们要将用户类中获取到的信息返给前端,以便于前端能拿到相应数据进行加工展示,这时我们需要进入到redis拿到对象相关信息。

从redis获取对象信息:

User newUser = (User) redisTemplate.opsForValue().get(userName);
@Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication auth) throws IOException, ServletException {
        Object principal = auth.getPrincipal() ;
        String userName =  auth.getName() ;
        User newUser = (User) redisTemplate.opsForValue().get(userName);
        String token = newUser.getToken() ;

        List privilegeList = newUser.getPrivilegeList() ;

        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        response.setStatus(200);
        Map map = new HashMap<>(16) ;
        map.put("status", 200);
        map.put("msg", principal);
        map.put("token", token);
        map.put("privilege" , privilegeList) ;

        ObjectMapper om = new ObjectMapper();
        out.write(om.writeValueAsString(map));
        out.flush();
        out.close();
    }

二、第二阶段   用户访问其他接口

在用户成功实现登陆后,生成了token,接下来访问其他接口,以localhost:8080/sys/user/selectByName?userName=user端口为例,我们在其头部塞进token值后进行接口访问。

1. 首先进入过滤器JWTAuthorizationFilter类,对接口进行判断。

(1)首先读取端口头部信息,看是否带有token,如果没带token,返回,提示用户进行登陆

(2)如果带有token,那么进入token认证环节,token'认证无非就是头部token和redis中存储的token进行比对,如果比对成功那么表示认证通过,否则认证失败,这里我们注意到getAuthentication(tokenHeader)中只有一个token,那么我怎么拿到redis中user类呢,这里就需要对token进行解密处理,拿到用户登陆名,进而根据用户登录名取redis中获取token进行比对,也将用户角色进行存储。

public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    @Autowired
    JedisUtil jedisUtil ;

    @Autowired
    RedisTemplate redisTemplate ;

    @Autowired
    TokenUtil tokenUtil ;


    //redisTemplate注入
    public JWTAuthorizationFilter(AuthenticationManager authenticationManager , RedisTemplate redisTemplate , TokenUtil tokenUtil) {
        super(authenticationManager);
        this.redisTemplate = redisTemplate ;
        this.tokenUtil = tokenUtil ;
    }

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

        String tokenHeader = request.getHeader(JwtUtils.TOKEN_HEADER);
        // 如果请求头中没有Authorization信息则直接放行了
        if (tokenHeader == null) {
            super.doFilterInternal(request, response, chain);
            return;
        }
        // 如果请求头中有token,则进行解析
        SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
        super.doFilterInternal(request, response, chain);
    }

    // 从token中获取用户信息
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
        //解析token,拿到username
        DecodedJWT jwt = tokenUtil.deToken(tokenHeader) ;
        String userName = jwt.getClaim("username").asString() ;
        if(userName.equals(null)){
            return null ;
        }
        //去redis里面拿token 确认redis中存在和token对应的值
        ValueOperations UserVO = redisTemplate.opsForValue();
        User newUser = (User)  UserVO.get(userName) ;
        String redisToken = newUser.getToken() ;
        List grantedAuthorities = newUser.getGrantedAuthorities() ;
        if (redisToken.equals(tokenHeader)){
            return new UsernamePasswordAuthenticationToken(userName, null, grantedAuthorities);
        }
        return null;
    }

}

在实现过滤类中嵌入redisTemplate的时候出现了redisTemplate注入为空的问题,网上搜索了很多方案,才发现这个问题出现是因为过滤器生效得太快 导致redisTempalte注入来不及,从而出现注入为空的情况(大致是这个意思,后期重点学习注解和Spring Ioc容器),所以这里需要在过滤器的构造函数中对redisTemplate进行初始化,同时对WebConfig进行修改:

 .addFilter(new JWTAuthorizationFilter(authenticationManager() ,redisTemplate , tokenUtil ))

2. 过滤器类成功通过用户token认证后进入FilterInvocationSecurityMetadataSource类,功能是实现当前用户访问资源的URL和数据库中该资源对应的URL得匹配,以及获取数据库中该资源对应的角色,然后返回到下一个类中进行最终权限匹配。

(1)从接口访问信息中拿到接口要访问的资源,同时也从头部信息中拿到token

(2)将token进行解析,拿到用户登陆信息,进而访问redis拿到访问对象拥有的资源信息

(3)用当前访问接口的资源信息和用户具备的资源信息进行匹配,如果匹配上了,紧接着我们通过此资源信息拿到对应的角色,然后返回到下一个类中进行最终权限匹配。

 @Override
    public Collection getAttributes(Object o) throws IllegalArgumentException {
        /**
         * 从参数中获取当前请求的URL
         */
        String requestUrl = ((FilterInvocation) o).getRequestUrl() ;
        int len = requestUrl.indexOf("?");
        if(len != -1){
            requestUrl = requestUrl.substring(0,len);
        }
        String token = ((FilterInvocation) o).getRequest().getHeader("token") ;
        DecodedJWT jwt = tokenUtil.deToken(token) ;
        String userName = jwt.getClaim("username").asString() ;

        //从redis缓存中拿到当前对象拥有的资源访问权限,并和当前请求接口匹配
        User newUser = (User) redisTemplate.opsForValue().get(userName);
        List privilegeList = newUser.getPrivilegeList() ;

        for(Privilege privilegeUrl : privilegeList) {
            //将数据库中访问资源的URL与当前访问的URL进行ant风格匹配
            if (antPathMatcher.match(privilegeUrl.getPrivilegeUrl() , requestUrl)) {
                //获取访问此资源需要的角色
                Role role = roleMapper.selectByPrivilegeUrl(requestUrl) ;
                return SecurityConfig.createList(role.getRole()) ;
            }
        }
        throw new AccessDeniedException("权限不足");

    }

3. 紧接着进入最终验证环节,进入到AccessDecisionManager类中,auth对象中信息是过滤器那边返回过来的用户登陆认证通过的信息,包含用户应该具备的角色;ca对象中包含当前接口访问资源所需要的角色信息,这里就进行最终匹配,如果匹配成功,那么就验证通过,接口正常处理业务层代码逻辑,如果匹配失败,那么就提示权限不够的信息。

 @Override
    public void decide(Authentication auth, Object o, Collection ca) throws AccessDeniedException, InsufficientAuthenticationException {
        Collection<? extends GrantedAuthority> auths = auth.getAuthorities() ;
        for(ConfigAttribute configAttribute : ca) {
            if("ROLE_SUPERADMIN".equals(configAttribute.getAttribute()) && auth instanceof UsernamePasswordAuthenticationToken){
                return;
            }
            for(GrantedAuthority authority : auths){
                if(configAttribute.getAttribute().equals(authority.getAuthority())){
                    return;
                }
            }
            throw new AccessDeniedException("权限不足") ;
        }
    }

匹配成功:

至此,结束。

相关