zoukankan      html  css  js  c++  java
  • shiro的过滤器

    shiro的过滤器也是不多的我们可以自定义的方法,它的继承体系如下:

    另外UserFilter是继承于AccessControlFilter

    1、NameableFilter

    NameableFilter给Filter起个名字,如果没有设置默认就是FilterName;还记得之前的如authc吗?当我们组装过滤器链时会根据这个名字找到相应的过滤器实例;

    2、OncePerRequestFilter

    OncePerRequestFilter用于防止多次执行Filter的;也就是说一次请求只会走一次过滤器链;另外提供enabled属性,表示是否开启该过滤器实例,默认enabled=true表示开启,如果不想让某个过滤器工作,可以设置为false即可。

    3、ShiroFilter

    ShiroFilter是整个Shiro的入口点,用于过滤需要安全控制的请求进行处理,这个之前已经用过了。

    4、AdviceFilter

    AdviceFilter提供了AOP风格的支持,类似于SpringMVC中的Interceptor:

    Java代码  
    boolean preHandle(ServletRequest request, ServletResponse response) throws Exception  
    void postHandle(ServletRequest request, ServletResponse response) throws Exception  
    void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception;   

    preHandler:类似于AOP中的前置增强;在过滤器链执行之前执行;如果返回true则继续过滤器链;否则中断后续的过滤器链的执行直接返回;进行预处理(如基于表单的身份验证、授权)

    postHandle:类似于AOP中的后置返回增强;在过滤器链执行完成后执行;进行后处理(如记录执行时间之类的);

    afterCompletion:类似于AOP中的后置最终增强;即不管有没有异常都会执行;可以进行清理资源(如接触Subject与线程的绑定之类的);

    5、PathMatchingFilter

    PathMatchingFilter提供了基于Ant风格的请求路径匹配功能及过滤器参数解析的功能,如“roles[admin,user]”自动根据“,”分割解析到一个路径参数配置并绑定到相应的路径:

    Java代码  
    boolean pathsMatch(String path, ServletRequest request)  
    boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception   

    pathsMatch:该方法用于path与请求路径进行匹配的方法;如果匹配返回true;

    onPreHandle:在preHandle中,当pathsMatch匹配一个路径后,会调用opPreHandler方法并将路径绑定参数配置传给mappedValue;

    然后可以在这个方法中进行一些验证(如角色授权),如果验证失败可以返回false中断流程;默认返回true;也就是说子类可以只实现onPreHandle即可,无须实现preHandle。如果没有path与请求路径匹配,默认是通过的(即preHandle返回true)。

    6、AccessControlFilter

    AccessControlFilter提供了访问控制的基础功能;比如是否允许访问/当访问拒绝时如何处理等:

    Java代码  
    abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;  
    boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;  
    abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;   

    isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中过滤器参数部分,如果允许访问返回true,否则false;

    onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该过滤器实例已经处理了,将直接返回即可。

    onPreHandle会自动调用这两个方法决定是否继续处理:

    Java代码  
    boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {  
        return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);  
    }   

    另外AccessControlFilter还提供了如下方法用于处理如登录成功后/重定向到上一个请求: 

    Java代码  
    void setLoginUrl(String loginUrl) //身份验证时使用,默认/login.jsp  
    String getLoginUrl()  
    Subject getSubject(ServletRequest request, ServletResponse response) //获取Subject实例  
    boolean isLoginRequest(ServletRequest request, ServletResponse response)//当前请求是否是登录请求  
    void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException //将当前请求保存起来并重定向到登录页面  
    void saveRequest(ServletRequest request) //将请求保存起来,如登录成功后再重定向回该请求  
    void redirectToLogin(ServletRequest request, ServletResponse response) //重定向到登录页面

    比如基于表单的身份验证就需要使用这些功能。

    (1)如果我们想进行访问访问的控制就可以继承AccessControlFilter

    例如:Shiro提供roles拦截器,其验证用户拥有所有角色,没有提供验证用户拥有任意角色的拦截器。

    public class AnyRolesFilter extends AccessControlFilter {  
        private String unauthorizedUrl = "/unauthorized.jsp";  
        private String loginUrl = "/login.jsp";  
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {  
            String[] roles = (String[])mappedValue;  
            if(roles == null) {  
                return true;//如果没有设置角色参数,默认成功  
            }  
            for(String role : roles) {  
                if(getSubject(request, response).hasRole(role)) {  
                    return true;  
                }  
            }  
            return false;//跳到onAccessDenied处理  
        }  
      
        @Override  
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {  
            Subject subject = getSubject(request, response);  
            if (subject.getPrincipal() == null) {//表示没有登录,重定向到登录页面  
                saveRequest(request);  
                WebUtils.issueRedirect(request, response, loginUrl);  
            } else {  
                if (StringUtils.hasText(unauthorizedUrl)) {//如果有未授权页面跳转过去  
                    WebUtils.issueRedirect(request, response, unauthorizedUrl);  
                } else {//否则返回401未授权状态码  
                    WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);  
                }  
            }  
            return false;  
        }  
    }   

    流程:

    1、首先判断用户有没有任意角色,如果没有返回false,将到onAccessDenied进行处理;

    2、如果用户没有角色,接着判断用户有没有登录,如果没有登录先重定向到登录;

    3、如果用户没有角色且设置了未授权页面(unauthorizedUrl),那么重定向到未授权页面;否则直接返回401未授权错误码。

    (2)FormAuthenticationFilter基于表单登录拦截器

    public class FormLoginFilter extends PathMatchingFilter {  
        private String loginUrl = "/login.jsp";  
        private String successUrl = "/";  
        @Override  
        protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {  
            if(SecurityUtils.getSubject().isAuthenticated()) {  
                return true;//已经登录过  
            }  
            HttpServletRequest req = (HttpServletRequest) request;  
            HttpServletResponse resp = (HttpServletResponse) response;  
            if(isLoginRequest(req)) {  
                if("post".equalsIgnoreCase(req.getMethod())) {//form表单提交  
                    boolean loginSuccess = login(req); //登录  
                    if(loginSuccess) {  
                        return redirectToSuccessUrl(req, resp);  
                    }  
                }  
                return true;//继续过滤器链  
            } else {//保存当前地址并重定向到登录界面  
                saveRequestAndRedirectToLogin(req, resp);  
                return false;  
            }  
        }  
        private boolean redirectToSuccessUrl(HttpServletRequest req, HttpServletResponse resp) throws IOException {  
            WebUtils.redirectToSavedRequest(req, resp, successUrl);  
            return false;  
        }  
        private void saveRequestAndRedirectToLogin(HttpServletRequest req, HttpServletResponse resp) throws IOException {  
            WebUtils.saveRequest(req);  
            WebUtils.issueRedirect(req, resp, loginUrl);  
        }  
      
        private boolean login(HttpServletRequest req) {  
            String username = req.getParameter("username");  
            String password = req.getParameter("password");  
            try {  
                SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password));  
            } catch (Exception e) {  
                req.setAttribute("shiroLoginFailure", e.getClass());  
                return false;  
            }  
            return true;  
        }  
        private boolean isLoginRequest(HttpServletRequest req) {  
            return pathsMatch(loginUrl, WebUtils.getPathWithinApplication(req));  
        }  
    }   

    onPreHandle主要流程:

    1、首先判断是否已经登录过了,如果已经登录过了继续拦截器链即可;

    2、如果没有登录,看看是否是登录请求,如果是get方法的登录页面请求,则继续拦截器链(到请求页面),否则如果是get方法的其他页面请求则保存当前请求并重定向到登录页面;

    3、如果是post方法的登录页面表单提交请求,则收集用户名/密码登录即可,如果失败了保存错误消息到“shiroLoginFailure”并返回到登录页面;

    4、如果登录成功了,且之前有保存的请求,则重定向到之前的这个请求,否则到默认的成功页面。

    (3)CasFilter用于客户端从cas server获取到ticket后回调应用发起请求的时候被拦截,应用将ticket发送到cas server做验证

    (4)BasicHttpAuthenticationFilter通过Basic认证的方式登录(例如JWT token方案)

     JWTToken 

    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;
        }
    }

    BasicHttpAuthenticationFilter

    public class JWTFilter extends BasicHttpAuthenticationFilter {
    
        private Logger LOGGER = LoggerFactory.getLogger(this.getClass());
    
        /**
         * 判断用户是否想要登入。
         * 检测header里面是否包含Authorization字段即可
         */
        @Override
        protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
            HttpServletRequest req = (HttpServletRequest) request;
            String authorization = req.getHeader("Authorization");
            return authorization != null;
        }
    
        /**
         *
         */
        @Override
        protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            String authorization = httpServletRequest.getHeader("Authorization");
    
            JWTToken token = new JWTToken(authorization);
            // 提交给realm进行登入,如果错误他会抛出异常并被捕获
            getSubject(request, response).login(token);
            // 如果没有抛出异常则代表登入成功,返回true
            return true;
        }
    
        /**
         * 这里我们详细说明下为什么最终返回的都是true,即允许访问
         * 例如我们提供一个地址 GET /article
         * 登入用户和游客看到的内容是不同的
         * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
         * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
         * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
         * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
         */
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            if (isLoginAttempt(request, response)) {
                try {
                    executeLogin(request, response);
                } catch (Exception e) {
                    response401(request, response);
                }
            }
            return true;
        }
    
        /**
         * 对跨域提供支持
         */
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            HttpServletResponse httpServletResponse = (HttpServletResponse) 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"));
            // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
            if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
                httpServletResponse.setStatus(HttpStatus.OK.value());
                return false;
            }
            return super.preHandle(request, response);
        }
    
        /**
         * 将非法请求跳转到 /401
         */
        private void response401(ServletRequest req, ServletResponse resp) {
            try {
                HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
                httpServletResponse.sendRedirect("/401");
            } catch (IOException e) {
                LOGGER.error(e.getMessage());
            }
        }
    }
    View Code

     AuthorizingRealm

    public class MyRealm extends AuthorizingRealm {
    
        private static final Logger LOGGER = LogManager.getLogger(MyRealm.class);
    
        private UserService userService;
    
        @Autowired
        public void setUserService(UserService userService) {
            this.userService = userService;
        }
    
        /**
         * 在AuthorizingRealm传入一个token的时候shiro都会去验证这个token是不是被shiro所支持,只有这个地方返回true才会去进行后面认证鉴权操作。
         * AuthorizingRealm的AuthenticationToken默认实现是UsernamePasswordToken,所以去验证是不是UsernamePasswordToken它的子类
         * 可是这里是我们自定义的AuthenticationToken实现,所以要重写这个方法。
         */
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof AuthenticationToken;
        }
    
        /**
         * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            String username = JWTUtil.getUsername(principals.toString());
            UserBean user = userService.getUser(username);
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            simpleAuthorizationInfo.addRole(user.getRole());
            Set<String> permission = new HashSet<>(Arrays.asList(user.getPermission().split(",")));
            simpleAuthorizationInfo.addStringPermissions(permission);
            return simpleAuthorizationInfo;
        }
    
        /**
         * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
            String token = (String) auth.getCredentials();
            // 解密获得username,用于和数据库进行对比
            String username = JWTUtil.getUsername(token);
            if (username == null) {
                throw new AuthenticationException("token invalid");
            }
    
            UserBean userBean = userService.getUser(username);
            if (userBean == null) {
                throw new AuthenticationException("User didn't existed!");
            }
    
            if (! JWTUtil.verify(token, username, userBean.getPassword())) {
                throw new AuthenticationException("Username or password error");
            }
    
            return new SimpleAuthenticationInfo(token, token, "my_realm");
        }
    }
    View Code

    JWTUtil

    public class JWTUtil {
    
        // 过期时间60分钟
        private static final long EXPIRE_TIME = 60*60*1000;
    
        /**
         * 校验token是否正确
         * @param token 密钥
         * @param secret 用户的密码
         * @return 是否正确
         */
        public static boolean verify(String token, String username, String secret) {
            try {
                Algorithm algorithm = Algorithm.HMAC256(secret);
                JWTVerifier verifier = JWT.require(algorithm)
                        .withClaim("username", username)
                        .build();
                DecodedJWT jwt = verifier.verify(token);
                return true;
            } catch (Exception exception) {
                return false;
            }
        }
    
        /**
         * 获得token中的信息无需secret解密也能获得
         * @return token中包含的用户名
         */
        public static String getUsername(String token) {
            try {
                DecodedJWT jwt = JWT.decode(token);
                return jwt.getClaim("username").asString();
            } catch (JWTDecodeException e) {
                return null;
            }
        }
    
        /**
         * 生成签名,5min后过期
         * @param username 用户名
         * @param secret 用户的密码
         * @return 加密的token
         */
        public static String sign(String username, String secret) {
            try {
                Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
                Algorithm algorithm = Algorithm.HMAC256(secret);
                // 附带username信息
                return JWT.create()
                        .withClaim("username", username)
                        .withExpiresAt(date)
                        .sign(algorithm);
            } catch (UnsupportedEncodingException e) {
                return null;
            }
        }
    }
    View Code

     过程总结:

    当浏览器带着token过来,被JWTFilter拦截,从请求中拿到token,执行登录操作,这是会转给AuthorizingRealm,它的认证方法doGetAuthenticationInfo会对token进行校验,校验通过后获取token中的用户名信息;

    当需要鉴权的时候doGetAuthorizationInfo会由用户名信息从数据库去查询角色和权限。返回进行比对。

    部分参考:第八章 拦截器机制——《跟我学Shiro》

  • 相关阅读:
    java类的继承
    Java数组运算
    Eclipse问题提示
    oracle的insert的时候&符号如何插入(转义)
    修改数据库用户名--CMD环境执行有效
    java环境变量设置
    当前时间时间戳以及10分钟后的时间戳
    复选框事件
    分享到
    多物体运动
  • 原文地址:https://www.cnblogs.com/xiangkejin/p/8974967.html
Copyright © 2011-2022 走看看