10

关于验证大致分为两个方面:

  1. 用户登录时的验证;
  2. 用户登录后每次访问时的权限认证

主要解决方法:使用自定义的Shiro Filter

项目搭建:

这是一个spring-boot 的web项目,不了解spring-boot的项目搭建,请google。
  • pom.mx引入相关jar


 <!-- shiro 权限管理 -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>${shiro.version}</version>
    </dependency>
 <!-- JWT -->
     <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency>
  • Shrio 的相关配置

划重点!!自定义了一个Filter

filterMap.put("JWTFilter", new JWTFilter());

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 添加自己的过滤器并且取名为JWTFilter
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("JWTFilter", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        /*
         * 自定义url规则
         * http://shiro.apache.org/web.html#urls-
         */
        Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
        filterChainDefinitionMap.put("/**", "JWTFilter");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }


    /**
     * securityManager 不用直接注入shiroDBRealm,可能会导致事务失效
     * 解决方法见 handleContextRefresh
     * http://www.debugrun.com/a/NKS9EJQ.html
     */
    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(TokenRealm tokenRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(tokenRealm);
        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);
        return manager;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean(name = "TokenRealm")
    @DependsOn("lifecycleBeanPostProcessor")
    public TokenRealm tokenRealm() {
        return new TokenRealm();
    }

    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return new AuthorizationAttributeSourceAdvisor();
    }
}
  • 自定义Shrio filter

执行顺序:preHandle -> doFilterInternal -> executeLogin -> onLoginSuccess

主要判断是不是登录请求的是 doFilterInternal


public class JWTFilter extends BasicHttpAuthenticationFilter {

    /**
     * 自定义执行登录的方法
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws IOException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        UsernamePasswordToken usernamePasswordToken = JSON.parseObject(httpServletRequest.getInputStream(), UsernamePasswordToken.class);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        Subject subject = this.getSubject(request, response);
        subject.login(usernamePasswordToken);
        return this.onLoginSuccess(usernamePasswordToken, subject, request, response);
        //错误抛出异常
    }

    /**
     * 最先执行的方法
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        return super.preHandle(request, response);
    }

    /**
     * 登录成功后登录的操作
     * 加上jwt 的header
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        String jwtToken = Jwts.builder()
                .setId(token.getPrincipal().toString())
                .setExpiration(DateTime.now().plusMinutes(30).toDate())
                .signWith(SignatureAlgorithm.HS256, JWTCost.signatureKey)
                .compact();
        httpServletResponse.addHeader(AUTHORIZATION_HEADER, jwtToken);
        return true;
    }

    /**
     * 登录以及校验的主要流程
     * 判断是否是登录,或者是登陆后普通的一次请求
     */
    @Override
    public void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;

        String servletPath = httpServletRequest.getServletPath();
        if (StringUtils.equals(servletPath, "/login")) {
            //执行登录
            this.executeLogin(servletRequest, servletResponse);
        } else {
            String authenticationHeader = httpServletRequest.getHeader(AUTHORIZATION_HEADER);
            if (StringUtils.isNotEmpty(authenticationHeader)) {

                Claims body = Jwts.parser()
                        .setSigningKey(JWTCost.signatureKey)
                        .parseClaimsJws(authenticationHeader)
                        .getBody();
                if (body != null) {
                    //更新token
                    body.setExpiration(DateTime.now().plusMinutes(30).toDate());
                    String updateToken = Jwts.builder().setClaims(body).compact();
                    httpServletResponse.addHeader(AUTHORIZATION_HEADER, updateToken);

                    //添加用户凭证
                    PrincipalCollection principals = new SimplePrincipalCollection(body.getId(), JWTCost.UserNamePasswordRealm);//拼装shiro用户信息
                    WebSubject.Builder builder = new WebSubject.Builder(servletRequest, servletResponse);
                    builder.principals(principals);
                    builder.authenticated(true);
                    builder.sessionCreationEnabled(false);
                    WebSubject subject = builder.buildWebSubject();
                    //塞入容器,统一调用
                    ThreadContext.bind(subject);
                    filterChain.doFilter(httpServletRequest, httpServletResponse);
                }
            } else {
                httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value());
            }
        }
    }
}
  • 登录失败处理

处理Shrio异常

@RestControllerAdvice
public class GlobalControllerExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    public Object allExceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        String message = exception.getCause().getMessage();
        LogUtil.error(message);
        return new ResultInfo(exception.getClass().getName(), message);
    }

    /*=========== Shiro 异常拦截==============*/

    @ExceptionHandler(value = IncorrectCredentialsException.class)
    public String IncorrectCredentialsException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        return "IncorrectCredentialsException";
    }

    @ExceptionHandler(value = UnknownAccountException.class)
    public String UnknownAccountException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        return "UnknownAccountException";
    }

    @ExceptionHandler(value = LockedAccountException.class)
    public String LockedAccountException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        return "LockedAccountException";
    }

    @ExceptionHandler(value = ExcessiveAttemptsException.class)
    public String ExcessiveAttemptsException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        return "ExcessiveAttemptsException";
    }

    @ExceptionHandler(value = AuthenticationException.class)
    public String AuthenticationException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        return "AuthenticationException";
    }

    @ExceptionHandler(value = UnauthorizedException.class)
    public String UnauthorizedException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        return "UnauthorizedException";
    }
}

处理JWT异常

这是个坑,因为是在filter内发生的异常,@ExceptionHandler是截获不到的。

/**
 * 截获spring boot Error页面
 */
@RestController
public class GlobalExceptionHandler implements ErrorController {
    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping(value = "/error")
    public Object error(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 错误处理逻辑
        Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
        Throwable cause = exception.getCause();
        if (cause instanceof ExpiredJwtException) {
            response.setStatus(HttpStatus.GATEWAY_TIMEOUT.value());
            return new ResultInfo("ExpiredJwtException", cause.getMessage());
        }
        if (cause instanceof MalformedJwtException) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            return new ResultInfo("MalformedJwtException", cause.getMessage());
        }
        return new ResultInfo(cause.getCause().getMessage(), cause.getMessage());
    }
}

关于权限等授权信息,可以直接放到Redis中实现缓存。我认为也是不错的。

源码奉上:githup-shiro分支 :温馨提示:平时测试代码可能比较乱。

如果更好的实现,让我学习,让我我进步,请联系我。

workabee
25 声望2 粉丝