Shiro前后端分离流程

1.自定义filter

拦截所有携带token的请求, 调用自定义realm,判断token是否正确,不正确realm抛出异常,在filter中被捕获,重定向至token不正确界面

重写了三个方法:

1》isAccessAllowed:如果带有 token,则对 token 进行检查,否则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true(放行)

 2》isLoginAttempt:被isAccessAllowed调用,检测 header 里面是否包含 Token 字段。

3》executeLogin:实际的登录方法,当有token时,被isaccessallwed调用,执行subject.login方法,

即调用自定义realm中的doGetAuthenticationInfo方法,如果登录成功,isaccessallwed就返回true,如果登录失败,realm中抛出异常,抛到subject.login,被try  catch捕获,重定向到无权限界面(因为filer父类BasicHttpAuthenticationFilter没有抛出异常,所以这里不能抛出,也就不能给springboot的异常处理器处理)

4》prehandle  处理跨域

package com.hxut.rj1192.dormitory2.filter;


import com.hxut.rj1192.dormitory2.pojo.JWTToken;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

//       所有的请求都会到达这个过滤器处理。
//        我们需要重写几个方法:
//
//        isAccessAllowed:是否允许访问。如果带有 token,则对 token 进行检查,否则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true

//        isLoginAttempt:检测 header 里面是否包含 Token 字段。

//        executeLogin:实际的登录方法

//        preHandle:拦截器的前置拦截,因为我们是前后端分析项目,项目中除了需要跨域全局配置之外,我们再拦截器中也需要提供跨域支持。这样,拦截器才不会在进入Controller之前就被限制了。

public class JWTFilter extends BasicHttpAuthenticationFilter {

    //是否允许访问,如果带有 token,则对 token 进行检查 (true即允许通过,false不通过),否则直接通过
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        System.out.println("JWTFilter的isAccessAllowed方法  登录验证 调用isLoginAttempt方法判断是否有jwt 没有就放行有就验证jwt返回true 否则返回false");
        //判断请求的请求头是否带上 token
        if (isLoginAttempt(request, response)) {
            //如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
            try {
                //  因为父类没有抛出异常,所以子类也不能抛出异常
                executeLogin(request, response);
                return true;
            } catch (AuthenticationException e) {
                //token 错误
                responseError(response, e.getMessage());
            }
        }
        //如果请求头不存在 Token,则可能是执行登录操作或者是游客状态访问,无需检查 token,直接返回 true
        return true;
    }

    /**
     * 判断用户是否想要登入。
     * 检测 header 里面是否包含 Token 字段
     */

    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader("Authorization");
        System.out.println("JWTFilter的isLoginAttempt方法  判断请求头中有没有jwt" + (token == null));
        return token != null;
    }

    /*
     * 实际的登录方法,这里我们重写了这个方法
     * 当登录失败时,realm 返回false login 会抛出异常,被上面调用executeLogin isAccessAllowed 方法捕获
     * */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) {
        System.out.println("JWTFilter的executeLogin方法  验证token");
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader("Authorization");
        System.out.println("请求头中的token    " + token);
        JWTToken jwt = new JWTToken(token);
        //交给自定义的realm对象去登录,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwt);
        return true;
    }

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        res.setHeader("Access-control-Allow-Origin", req.getHeader("Origin"));
        res.setHeader("Access-control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        res.setHeader("Access-control-Allow-Headers", req.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
            res.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 将非法请求跳转到 /unauthorized/**
     */
    private void responseError(ServletResponse response, String message) {
        System.out.println("跳转到错误页面");
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        //设置编码,否则中文字符在重定向时会变为空字符串
        try {
            message = URLEncoder.encode(message, "UTF-8");
            httpServletResponse.sendRedirect("/unauthorized/" + message);
        } catch (IOException e) {
        }
    }
}

2.自定义realm 

和不分离的区别就是doGetAuthenticationInfo,从接收默认token变成了接收自定义token,doGetAuthenticationInfo方法调用jwtutil 验证token ,而不是访问数据库查询用户名密码。

package com.hxut.rj1192.dormitory2.realm;


import com.hxut.rj1192.dormitory2.mapper.UserMapper;
import com.hxut.rj1192.dormitory2.pojo.JWTToken;
import com.hxut.rj1192.dormitory2.pojo.User;
import com.hxut.rj1192.dormitory2.service.impl.CheckloginService;
import com.hxut.rj1192.dormitory2.util.JWTUtil;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Set;

@Component
public class AccountRealm extends AuthorizingRealm {
    //根据token判断此Authenticator是否使用该realm
    //必须重写不然shiro会报错
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    /**
     * 只有当需要检测用户权限的时候才会调用此方法,例如@RequiresRoles,@RequiresPermissions之类的
     */
    @Autowired
    UserMapper userMapper;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("自定义realm的doGetAuthorizationInfo执行了~~~~~查询用户权限");
        String token = principals.toString();
        String username = JWTUtil.getUsername(token);
        System.out.println("自定义realm的doGetAuthorizationInfo执行了~~~~~查询用户权限,查到的用户名" + username);
        System.out.println("自定义realm的doGetAuthorizationInfo执行了~~~~~查询用户权限,查到的角色名" + userMapper.queryrole(username));
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //查询数据库获取用户的角色
        info.addRole(userMapper.queryrole(username));
        //查询数据库获取用户的权限
        Set<String> querypermission = userMapper.querypermission(username);
        for (String temp : querypermission) {
            info.addStringPermission(temp);
        }
        return info;
    }


    /**
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可,在需要用户认证和鉴权的时候才会调用
     */
    @Autowired
    CheckloginService checkloginService;

    // 获取token中的用户名,去数据库查询相应信息,如果为空,代表没有该用户
    // 验证token是否正确
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String jwt = (String) token.getCredentials();
        //decode时候出错,可能是token的长度和规定好的不一样了
        String username = JWTUtil.getUsername(jwt);
        System.out.println("accountrealm的 doGetAuthenticationInfo方法(验证token)中获取到的用户名" + username);
        JWTUtil.verify(jwt);//如果认证失败,会抛一个AuthenticationException错误
        User user = userMapper.querybyname(username);
        if (user == null) {
            throw new AuthenticationException("该用户不存在");
        }
//        前后端不分离,返回的是用户名,密码 ,realm名
        return new SimpleAuthenticationInfo(jwt, jwt, "AccountRealm");
    }
}


 3.jwtutil 生成token,验证token

package com.hxut.rj1192.dormitory2.util;

import com.alibaba.fastjson.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.hxut.rj1192.dormitory2.pojo.ReturnMap;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.shiro.authc.AuthenticationException;
import org.springframework.context.annotation.Configuration;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class JWTUtil {
    //设置过期时间  一分钟过期
    private static final long EXPIRE_DATE = 60 * 1000 * 10;
    // token 是给前端的一段有我自己标识(TOKEN_SECRET 标识这个toekn确实是服务器生成的 )的密码,
    // 因为前后端分离的项目没有session cookie,就需要用token来标识用户,替代原来的session 和cookie
    // 如果其他人想要伪造token,则必须要知道我的标识 即TOKEN_SECRET,所以别人不能伪造
//    如果更安全一点,可以使用md5对普通生成的toekn二次加密加盐,即使黑客得到了toekn,
//    它也不能破解出 toekn第二部分携带的信息(但是可以伪造用户去请求服务器(toekn就是密码,密码泄露了,肯定能登录的),所以toekn要设置有效期)
    private static final String TOKEN_SECRET = "ZYKZYKZYKZYK";

    public static String createToken(String username, String password) {
        String token = "";
        try {
            //过期时间
            Date date = new Date(System.currentTimeMillis() + EXPIRE_DATE);
            //秘钥及加密算法
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            //设置头部信息
            Map<String, Object> header = new HashMap<>();
            header.put("typ", "JWT");
            header.put("alg", "HS256");
            //携带username,password信息,生成签名
            token = JWT.create()
                    .withHeader(header)
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return token;
    }

    //无需解密也可以获取token的信息
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }

    }

    public static boolean verify(String token) {
        /**
         * @desc 验证token,通过返回true
         * @params [token]需要校验的串
         **/
        try {
            DecodedJWT jwt2 = JWT.decode(token);
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            JWTVerifier verifier = JWT.require(algorithm).build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (TokenExpiredException e) {
            throw new AuthenticationException(JSON.toJSONString( ReturnMap.fail("token过期,登录失败","-11")));
        }
        catch(SignatureVerificationException e){
            throw new AuthenticationException(JSON.toJSONString( ReturnMap.fail("token可能被篡改,或token错误,登录失败","-11")));
        }
    }
}

4.自定义Token  类

     调用subject.logn方法 ,需要一个 AuthenticationToken  类,来存储用户信息,不设置就是默认的,有用户名密码等,但是我要验证的是jwt,不是用户名和密码,所以要写个新的AuthenticationToken类,来作为subject.login方法的参数,传递给realm中doGetAuthenticationInfo

package com.hxut.rj1192.dormitory2.pojo;

import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;

/*
    JwtToken代替原生的UsernamePasswordToken ,所以要继承AuthenticationToken类
 */
public class JWTToken implements AuthenticationToken {
    private String jwt;

    public JWTToken(String jwt) {
        this.jwt = jwt;
    }

    //获取用户名 ,这里直接返回token
    @Override
    public Object getPrincipal() {
        return jwt;
    }

    //返回密码,这里直接返回token
    @Override
    public Object getCredentials() {
        return jwt;
    }
}

5.shiroconfig

1》设置自定义的realm

2》设置关闭session

3.》开启shiro注解

4》设置拦截,拦截除了unauthorized/的所有路径,交给自定义filter去判断是否放行(/unauthorized/是没有权限时重定向的路径,如果没有放行,则带有token,被拦截,验证realm,抛出异常,跳转至/unauthorized重定向界面,被拦截,验证realm.....无限循环了)

package com.hxut.rj1192.dormitory2.config;


import com.hxut.rj1192.dormitory2.filter.JWTFilter;
import com.hxut.rj1192.dormitory2.realm.AccountRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager(AccountRealm accountRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置自定义 realm
        securityManager.setRealm(accountRealm);

        //关闭session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }

    /**
     * 先走 filter ,filter 如果检测到请求头存在 token,则用 token 去 login,走 Realm 去验证
     */
    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        //设置自己的过滤器并且取名为jwt
        filterMap.put("jwt", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        // 设置无权限时跳转的 url;
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
        Map<String, String> filterRuleMap = new HashMap<>();
        // 所有请求通过我们自己的JWT Filter
        filterRuleMap.put("/**", "jwt");
        // 除了无权限时跳转的路径
        filterRuleMap.put("/unauthorized/**", "anon");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 添加注解支持,如果不加的话很有可能注解失效
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

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

 5.token验证失败重定向的路径

package com.hxut.rj1192.dormitory2.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.UnsupportedEncodingException;

@Controller
public class NoQuanXian {
    @ResponseBody
    @RequestMapping(path = "/unauthorized/{message}")
    public String unauthorized(@PathVariable String message) throws UnsupportedEncodingException {
        System.out.println(message+"message");
        return message;
    }
}

6.权限不足的路径,或者是未携带token访问需要token的路径的全局异常处理类(也可以处理其他异常)

package com.hxut.rj1192.dormitory2.controller;


import com.alibaba.fastjson.JSON;
import com.hxut.rj1192.dormitory2.pojo.ReturnMap;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
public class GlobalExeception {
    @ResponseBody
    @ExceptionHandler
    public String doexection(Exception e) {
        System.out.println(e);
        if (e instanceof UnauthenticatedException) {
            return JSON.toJSONString(ReturnMap.fail("当前用户未认证", "-10"));
        } else if (e instanceof UnauthorizedException) {
            return JSON.toJSONString(ReturnMap.fail("当前用户权限不足", "-10"));
        }
        else if (e instanceof HttpMessageNotReadableException) {
            return JSON.toJSONString(ReturnMap.fail("缺少必要参数或参数格式不正确", "-10"));
        }
         else if(e instanceof HttpRequestMethodNotSupportedException){
            return JSON.toJSONString(ReturnMap.fail("请求方式不正确", "-10"));
        }
        return null;
    }
}

 有点问题:每次带token的请求都会请求一次mysql,最好先查redis,没有再查mysql

猜你喜欢

转载自blog.csdn.net/sharesb/article/details/127980903