shiro在springcloud中的权限路由配置

shiro在springboot中一般为统一配置,每个需要的请求都可以用注解拦截和认证。在springcloud中,你可以单拉一个shiro模块,每个模块pom文件引入这个shiro模块,也可以只在网关模块配置shiro。

如果只是实现请求拦截,spring拦截器和注解AOP也方便实现。但本文拉入了shiro,原理是同spirng拦截器实现的,具体如下。

引入shiro/jwt/session实现所需依赖:

<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis-spring-boot-starter</artifactId>
    <version>3.2.1</version>
</dependency>
<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

jwt工具类:

@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "xzc.jwt")
public class JwtUtils {
 
    private String secret;
    private long expire;
    private String header;
 
    /**
     * 生成jwt token
     */
    public String generateToken(long userId) {
        Date nowDate = new Date();
        //过期时间
        Date expireDate = new Date(nowDate.getTime() + expire * 1000);
 
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(userId+"")
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }
 
    public Claims getClaimByToken(String token) {
 
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
 
    }
 
    /**
     * token是否过期
     * @return  true:过期
     */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }
}

shiroConfig配置:


@Configuration
public class ShiroConfig {
    @Autowired
    JwtFilter jwtFilter;
 
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager,
                                                         ShiroFilterChainDefinition shiroFilterChainDefinition) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(defaultWebSecurityManager);
        Map<String, String> map = shiroFilterChainDefinition.getFilterChainMap();
//        map.put("/user/test", "perms[user:add]"); 这里不用,而采用AuthParams列表。
        bean.setFilterChainDefinitionMap(map);
        Map<String, Filter> filters = new HashMap<>();
        filters.put("jwt", jwtFilter);
        bean.setFilters(filters);
        bean.setLoginUrl("/user/goLogin");
        bean.setUnauthorizedUrl("/user/noauth");
        return bean;
    }
 
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/user/test", "jwt"); 
        filterMap.put("/user/test2", "jwt");
        chainDefinition.addPathDefinitions(filterMap);
        return chainDefinition;
    }
 
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(AccountRealm accountRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(accountRealm);
        return securityManager;
    }
 
    @Bean
    public AccountRealm accountRealm() {
        return new AccountRealm();
    }
}

AccountRealm配置在本篇中没什么作用,所以public class AccountRealm extends AuthorizingRealm 简单实现一下就行。

实现逻辑在shiroConfig所引入的JwtFilter类,所有要做权限拦截的路由都是在shiroFilterChainDefinition()方法通过filterMap.put("/user/test", "jwt");添加,如果嫌太多也可以用通配符"/**/t"表示所有末尾加"/t"的路径,然后shiroFilterFactoryBean()方法里filters.put("jwt", jwtFilter);实现。

@Component
public class JwtFilter extends AuthenticatingFilter {
 
    @Autowired
    JwtUtils jwtUtils;
    @Autowired
    AuthParams authParams;
 
    @Autowired
    RestTemplate restTemplate;
 
    @Autowired
    StringRedisTemplate stringRedisTemplate;
 
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
 
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if (StringUtils.isEmpty(jwt)) {
            return null;
        }
 
        return new JwtToken(jwt);
    }
 
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
 
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if (StringUtils.isEmpty(jwt)) {
            servletResponse.setContentType("0");  //通过返回前端type判断jwt错误类型
            return false;
        } else {
            Claims claim = jwtUtils.getClaimByToken(jwt);
            if (claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
                servletResponse.setContentType("1");
                throw new ExpiredCredentialsException("token已过期,请重新登录");
            }
            String requestURI = ((HttpServletRequest) servletRequest).getRequestURI();
            String param = authParams.GetPerms().get(requestURI);
 
            String userId = claim.getSubject();
            HttpSession httpSession = ((ShiroHttpServletRequest) servletRequest).getSession();
            Object userObj = httpSession.getAttribute("user");
            Map<String, Object> userMap;
            if (userObj == null) {
                User user = restTemplate.postForObject("http://user/getbyid", Long.valueOf(userId), User.class);
                if (user == null) {
                    servletResponse.setContentType("2");
                    return false;
                }
                String jsonStr = JSONObject.toJSONString(user);
                userMap = JSONObject.parseObject(jsonStr, Map.class);
                httpSession.setAttribute("user", user);
            } else {
                userMap = JSONObject.parseObject(userObj.toString(), Map.class);
            }
            if (param == null) {
                return true;
            }
            if (userMap.get("promission").indexOf(param)==-1) {
                servletResponse.setContentType("3");
                return false;
            }
            return true;
        }
    }
 
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
 
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
 
        Throwable throwable = e.getCause() == null ? e : e.getCause();
        Result result = Result.error(throwable.getMessage());
        String json = JSONUtil.toJsonStr(result);
 
        try {
            httpServletResponse.getWriter().print(json);
        } catch (IOException ioException) {
 
        }
        return false;
    }
 
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
 
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
            return false;
        }
 
        return super.preHandle(request, response);
    }
}
public class JwtToken implements AuthenticationToken {
    private String token;
    public JwtToken(String token) {
        this.token = token;
    }
    @Override
    public Object getPrincipal() {
        return token;
    }
    @Override
    public Object getCredentials() {
        return token;
    }
}
@Component
public class AuthParams {
 
    public Map<String,String> GetPerms(){
        Map<String,String> map=new HashMap<>();
        map.put("/user/test","user:add");
        map.put("/order/test2","order:delete");
        return map;
    }
}

onAccessDenied()判断逻辑:每个登录用户在请求中都会携带一个“Authorization”请求头,如果没有servletResponse.setContentType("0"),作用是约定“0”代表token为空,“1”表示已过期,“2”表示用户不存在,“3”代表没有操作权限。这个ContentType可给前端返回值的头部信息中获得,而实现相关的控制逻辑。

尝试通过httpSession获取当前用户,如果为空则通过token获取用户id,然后通过id重新获取该用户信息,并写入httpSession。

权限列表写在AuthParams里,通过判断当前用户权限列表里是否包含该请求路径权限值,验证是否具有操作权限。

当然也能用角色管理权限,例如map.put("/user/test","admin,student");不过可以用mysql管理,登录时把所有请求路径权限处理成("/user/test","admin,student")map集合,保存在httpSession里。那么判断就要改成:
 

if(param.indexOf(userMap.get("role"))==-1){
        servletResponse.setContentType("3");
        return false;
    }

方便操作的实现还是第一种,因为让mysql维护请求路径不太现实。没添加在AuthParams里的请求路径直接放行,而当程序员把请求权限写入后,也需要在添加请求权限的页面上添加入库。添加页面包括请求名称/权限值,以及父级路由,这样在编辑角色权限时,也能在权限路由子级下显示细粒度的权限请求。那么未指定父级路由的,可在编辑角色权限页面单独显示即可,全选全不选或点选。注册用户应该有个默认角色,通常只有管理员可以分配角色,用户登录时就可以根据角色加载promission的集合了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值