pingssys-shiro jwt无状态权限认证

1.背景

  • 在开发pingss-sys脚手架(项目地址)时,需要在微服务分布式环境中管理权限。有两种比较通用模式:
    • 基于session,把session序列化,以实现多系统的session共享。可以采用shiro+redis实现,有现成的jar可使用
    • 基于jwt,使用无状态的权限认证
  • 鉴于jwt无状态的权限认证在多个平台下适用性更好,本人采用了此种模式,结合shiro实现

2.思路

pingssys-shiro jwt无状态权限认证_第1张图片

3.步骤

A.实现AuthenticationToken,自定义JwtToken

/**
 *********************************************************
 ** @desc  : JwtToken
 ** @author   Pings
 ** @date     2019/1/23
 ** @version  v1.0
 * *******************************************************
 */
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;
    }
}

B.编写jwt工具,实现jwt加密及验证功能

/**
 *********************************************************
 ** @desc  : JwtUtil
 ** @author   Pings
 ** @date     2019/1/23
 ** @version  v1.0
 * *******************************************************
 */
public class JwtUtil {

    //**用户名称的key
    private static final String USER_NAME = "userName";
    //**默认的jwt加密secret
    private static final String DEFAULT_SECRET = "pingssys";
    //**默认的过期时间5分钟
    private static final long DEFAULT_EXPIRE_TIME = 5;

    /**
     *********************************************************
     ** @desc :生成访问令牌
     ** @author Pings
     ** @date   2019/1/23
     ** @param  secret         secret
     ** @param  userName       用户名
     ** @param  expiresTime    过期时间
     ** @return String
     * *******************************************************
     */
    public static String sign(String secret, String userName, long expiresTime) {
        //**过期时间
        expiresTime = expiresTime > 0 ? expiresTime : DEFAULT_EXPIRE_TIME;
        expiresTime = expiresTime * 60 * 1000;

        Algorithm algorithm = Algorithm.HMAC256(getSecret(secret, userName));
        return JWT.create().withClaim(USER_NAME, userName)
                  .withExpiresAt(new Date(currentTimeMillis + expiresTime)).sign(algorithm);
    }

    /**
     *********************************************************
     ** @desc : 校验token
     ** @author Pings
     ** @date   2019/1/23
     ** @param  token    令牌
     ** @param  secret   secret
     ** @return boolean
     * *******************************************************
     */
    public static boolean verify(String token, String secret) {
        Algorithm algorithm = Algorithm.HMAC256(getSecret(secret, JwtUtil.getUserName(token)));
        JWTVerifier verifier = JWT.require(algorithm).build();
        verifier.verify(token);
        return true;
    }

    /**
     *********************************************************
     ** @desc : 获取用户名称
     ** @author Pings
     ** @date   2019/1/23
     ** @param  token  令牌
     ** @return String
     * *******************************************************
     */
    public static String getUserName(String token) {
        Claim claim = decodeToken(token, jwt -> jwt.getClaim(USER_NAME));
        return claim == null ? null : claim.asString();
    }

    /**
     *********************************************************
     ** @desc :把访问令牌存放到响应的头信息中
     ** @author Pings
     ** @date   2019/3/21
     ** @param  response  响应
     ** @param  token     令牌
     * *******************************************************
     */
    public static void setHttpServletResponse(HttpServletResponse response, String token) {
        response.setHeader("Authorization", token);
        response.setHeader("Access-Control-Expose-Headers", "Authorization");
    }

    /**
     *********************************************************
     ** @desc : token解码
     ** @author Pings
     ** @date   2019/1/23
     ** @param  token  标记
     ** @return T
     * *******************************************************
     */
    private static  T decodeToken(String token, Function func) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return func.apply(jwt);
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    //**获取jwt加密secret
    private static String getSecret(String secret, String userName){
        return userName + (StringUtils.isNotBlank(secret) ? secret : DEFAULT_SECRET);
    }
}

C.自定义shiro filter

/**
 *********************************************************
 ** @desc  : JwtFilter
 ** @author   Pings
 ** @date     2019/1/23
 ** @version  v1.0
 * *******************************************************
 */
public class JwtFilter extends BasicHttpAuthenticationFilter {

    /**登录认证*/
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //**判断用户是否要登入
        if (this.isLoginAttempt(request, response)) {
            try {
                //**登录认证
                return this.executeLogin(request, response);
            } catch (Exception e) {
                this.response401(request, response, e.getMessage());
                return false;
            }
        }

        return true;
    }

    /**去掉调用executeLogin,避免循环调用doGetAuthenticationInfo方法*/
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        this.sendChallenge(request, response);
        return false;
    }

    /**检测Header里面是否包含Authorization字段*/
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        String token = this.getAuthzHeader(request);
        return token != null;
    }

    /**调用JwtRealm进行登录认证*/
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        //**获取token
        JwtToken token = new JwtToken(this.getAuthzHeader(request));
        //**提交给JwtRealm认证
        this.getSubject(request, response).login(token);
        //**没有抛出异常则代表登入成功
        return true;
    }

    /**401时直接返回Response信息*/
    private void response401(ServletRequest req, ServletResponse resp, String msg) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
        httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");

        ApiResponse response = new ApiResponse(HttpStatus.UNAUTHORIZED.value(), "Unauthorized: " + msg, null);
        String data = JSONObject.toJSONString(response);
        try(PrintWriter out = httpServletResponse.getWriter()) {
            out.append(data);
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    /**支持跨域*/
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
        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请求,返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }

        return super.preHandle(request, response);
    }
}

D.自定义shiro realm

/**
 *********************************************************
 ** @desc  : 自定义Realm
 ** @author   Pings
 ** @date     2019/1/23
 ** @version  v1.0
 * *******************************************************
 */
public class JwtRealm extends AuthorizingRealm {

    @Value("${sys.jwt.secret}")
    private String secret;
    @Reference(version = "${sys.service.version}")
    private UserService userService;

    /**必须重写此方法,不然Shiro会报错*/
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**权限验证*/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        String userName = JwtUtil.getUserName(principals.toString());

        //**获取用户
        User user = this.userService.getByUserName(userName);

        //**用户角色
        Set roles = user.getRoles().stream().map(Role::getCode).collect(toSet());
        authorizationInfo.addRoles(roles);

        //**用户权限
        Set rights = user.getRoles().stream().map(Role::getRights).flatMap(List::stream).map(Right::getCode).collect(toSet());
        authorizationInfo.addStringPermissions(rights);

        return authorizationInfo;
    }

    /**登录验证*/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        //**获取用户名称
        String userName = JwtUtil.getUserName(token);
        //**用户名称为空
        if (StringUtils.isBlank(userName)) {
            throw new AuthenticationException("The account in Token is empty.");
        }

        //**获取用户
        User user = this.userService.getByUserName(userName);
        if (user == null) {
            throw new AuthenticationException("The account does not exist.");
        }

        //**登录认证
        if (JwtUtil.verify(token, userName, secret)) {
            return new SimpleAuthenticationInfo(token, token, "jwtRealm");
        }

        throw new AuthenticationException("Username or password error.");
    }

    /**管理员不验证权限*/
    @Override
    public  boolean isPermitted(PrincipalCollection principal, String permission){
        AuthorizationInfo info = this.getAuthorizationInfo(principal);
        return info.getRoles().contains("admin") || super.isPermitted(principal, permission);
    }

    /**管理员不验证角色*/
    @Override
    public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {
        AuthorizationInfo info = this.getAuthorizationInfo(principal);
        return info.getRoles().contains("admin") || super.hasRole(principal, roleIdentifier);
    }
}

E.配置shrio

/**
 *********************************************************
 ** @desc  : Shiro配置
 ** @author   Pings
 ** @date     2019/1/23
 ** @version  v1.0
 * *******************************************************
 */
@Configuration
public class ShiroConfig {

    @Bean
    public JwtRealm jwtRealm(){
        return new JwtRealm();
    }

    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(JwtRealm jwtRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        //**使用自定义JwtRealm
        manager.setRealm(jwtRealm);

        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        return manager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        //**添加自定义过滤器jwt
        Map filterMap = new LinkedHashMap<>();
        filterMap.put("jwt", new JwtFilter());
        factoryBean.setFilters(filterMap);

        factoryBean.setSecurityManager(securityManager);

        //**自定义url规则
        Map filterRuleMap = new LinkedHashMap<>();
        //不拦截请求swagger-ui页面请求
        filterRuleMap.put("/webjars/**", "anon");
        //jwt过滤器拦截请求
        filterRuleMap.put("/**", "jwt");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);

        return factoryBean;
    }

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

    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }

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

F.LoginController中编写登录方法

/**
 *********************************************************
 ** @desc : 登录
 ** @author Pings
 ** @date   2019/1/22
 ** @param  userName  用户名称
 ** @param  password  用户密码
 ** @return ApiResponse
 * *******************************************************
 */
@ApiOperation(value="登录", notes="验证用户名和密码")
@PostMapping(value = "/account")
public ApiResponse account(String userName, String password, HttpServletResponse response){
    if(StringUtils.isBlank(userName) || StringUtils.isBlank(password))
        throw new UnauthorizedException("用户名/密码不能为空");

    //**md5加密
    password = DigestUtils.md5DigestAsHex(password.getBytes());

    User user = this.userService.getByUserName(userName);
    if(user != null && user.getPassword().equals(password)) {
        JwtUtil.setHttpServletResponse(response, JwtUtil.sign(userName, password, expireTime));

        //**用户权限
        Set rights = user.getRoles().stream().map(Role::getRights).flatMap(List::stream).map(Right::getCode).collect(toSet());
        return new ApiResponse(200, "登录成功", rights);
    } else
        return new ApiResponse(500, "用户名/密码错误");
}

4.说明

  • dubbo分布式系统权限认证
    • 只要dubbo多个子系统签发token的方式相同,某个子系统签发的token即可访问所有其它的子系统
  • 存在的问题
    • 生成的token如果过期时间太短,则每次到期后,都需要用户重新登录
    • 生成的token如果过期时间太长,由于token签发后,在有效期内无法注销,存在安全隐患
    • 下一篇结合RefreshToken和AccessToken一起使用,解决上述两个问题

你可能感兴趣的:(微服务,PingsSys微服务脚手架)