首页 > 技术文章 > shrio总结

wq-9 2020-03-20 15:44 原文

 

AccessControlFilter(https://www.jianshu.com/p/9bfa22b0e905)

SpringBoot+Shiro学习之自定义拦截器管理在线用户(踢出用户)

 

应用场景

  1. 我们经常会有用到,当A 用户在北京登录 ,然后A用户在天津再登录 ,要踢出北京登录的状态。如果用户在北京重新登录,那么又要踢出天津的用户,这样反复。又或是需要限制同一用户的同时在线数量,超出限制后,踢出最先登录的或是踢出最后登录的。

  2. 第一个场景踢出用户是由用户触发的,有时候需要手动将某个在线用户踢出,也就是对当前在线用户的列表进行管理。

·························································································································································
个人博客:http://z77z.oschina.io/

此项目下载地址:https://git.oschina.net/z77z/springboot_mybatisplus
························································································································································

实现思路

spring security就直接提供了相应的功能;Shiro的话没有提供默认实现,不过可以很容易的在Shiro中加入这个功能。那就是使用shiro强大的自定义访问控制拦截器:AccessControlFilter,集成这个接口后要实现下面这三个方法。

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:会自动调用这两个方法决定是否继续处理;

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

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) //重定向到登录页面   

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

到此基本的拦截器就完事了,如果我们想进行访问的控制就可以继承AccessControlFilter;如果我们要添加一些通用数据我们可以直接继承PathMatchingFilter。

下面就是我实现的访问控制拦截器:KickoutSessionControlFilter:

/**
 * @author 作者 z77z
 * @date 创建时间:2017年3月5日 下午1:16:38
 * 思路:
 * 1.读取当前登录用户名,获取在缓存中的sessionId队列
 * 2.判断队列的长度,大于最大登录限制的时候,按踢出规则
 *  将之前的sessionId中的session域中存入kickout:true,并更新队列缓存
 * 3.判断当前登录的session域中的kickout如果为true,
 * 想将其做退出登录处理,然后再重定向到踢出登录提示页面
 */
public class KickoutSessionControlFilter extends AccessControlFilter {

    private String kickoutUrl; //踢出后到的地址
    private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
    private int maxSession = 1; //同一个帐号最大会话数 默认1

    private SessionManager sessionManager;
    private Cache<String, Deque<Serializable>> cache;

    public void setKickoutUrl(String kickoutUrl) {
        this.kickoutUrl = kickoutUrl;
    }

    public void setKickoutAfter(boolean kickoutAfter) {
        this.kickoutAfter = kickoutAfter;
    }

    public void setMaxSession(int maxSession) {
        this.maxSession = maxSession;
    }

    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }
    //设置Cache的key的前缀
    public void setCacheManager(CacheManager cacheManager) {
        this.cache = cacheManager.getCache("shiro_redis_cache");
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        if(!subject.isAuthenticated() && !subject.isRemembered()) {
            //如果没有登录,直接进行之后的流程
            return true;
        }

        Session session = subject.getSession();
        SysUser user = (SysUser) subject.getPrincipal();
        String username = user.getNickname();
        Serializable sessionId = session.getId();

        //读取缓存   没有就存入
        Deque<Serializable> deque = cache.get(username);
        
        //如果队列里没有此sessionId,且用户没有被踢出;放入队列
        if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
            //将sessionId存入队列
            deque.push(sessionId);
            //将用户的sessionId队列缓存
            cache.put(username, deque);
        }

        //如果队列里的sessionId数超出最大会话数,开始踢人
        while(deque.size() > maxSession) {
            Serializable kickoutSessionId = null;
            if(kickoutAfter) { //如果踢出后者
                kickoutSessionId = deque.removeFirst();
            } else { //否则踢出前者
                kickoutSessionId = deque.removeLast();
            }
            //踢出后再更新下缓存队列
            cache.put(username, deque);
            
            
            try {
                //获取被踢出的sessionId的session对象
                Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                if(kickoutSession != null) {
                    //设置会话的kickout属性表示踢出了
                    kickoutSession.setAttribute("kickout", true);
                }
            } catch (Exception e) {//ignore exception
            }
        }

        //如果被踢出了,直接退出,重定向到踢出后的地址
        if ((Boolean)session.getAttribute("kickout")!=null&&(Boolean)session.getAttribute("kickout") == true) {
            //会话被踢出了
            try {
                //退出登录
                subject.logout();
            } catch (Exception e) { //ignore
            }
            saveRequest(request);
            //重定向
            WebUtils.issueRedirect(request, response, kickoutUrl);
            return false;
        }
        return true;
    }
}

将这个自定义的拦截器配置在ShiroConfig.java文件中:

/**
  * 限制同一账号登录同时登录人数控制
  * @return
  */
 public KickoutSessionControlFilter kickoutSessionControlFilter(){
    KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
    //使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
    //这里我们还是用之前shiro使用的redisManager()实现的cacheManager()缓存管理
    //也可以重新另写一个,重新配置缓存时间之类的自定义缓存属性
    kickoutSessionControlFilter.setCacheManager(cacheManager());
    //用于根据会话ID,获取会话进行踢出操作的;
    kickoutSessionControlFilter.setSessionManager(sessionManager());
    //是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;踢出顺序。
    kickoutSessionControlFilter.setKickoutAfter(false);
    //同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
    kickoutSessionControlFilter.setMaxSession(1);
    //被踢出后重定向到的地址;
    kickoutSessionControlFilter.setKickoutUrl("/kickout");
     return kickoutSessionControlFilter;
  }

将这个kickoutSessionControlFilter()注入到shiroFilterFactoryBean中:

//自定义拦截器
Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
//限制同一帐号同时在线的个数。
filtersMap.put("kickout", kickoutSessionControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);

由于我们链接权限的控制是动态存在数据库中的,这个可以去看我之前动态权限控制的博文,所以我们还要在数据库中修改链接的权限,将kickout这个自定义的权限配置在对应的链接上。如下图:

 
权限表

还要编写对应的被踢出的跳转页面:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://"
            + request.getServerName() + ":" + request.getServerPort()
            + path;
%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript"
    src="<%=basePath%>/static/js/jquery-1.11.3.js"></script>
<title>被踢出</title>
</head>
<body>
被踢出 或则在另一地方登录,或已经达到此账号登录上限被挤掉。
<input type="button" id="login" value="重新登录" />
</body>
<script type="text/javascript">
$("#login").click(function(){
    window.open("<%=basePath%>/login"); 
});
</script>
</html>

到此,第一个场景就实现了,写到这里实际第二个场景的实现思路已经就很明显了,可以通过sessionDAO获取到全部的shiro会话List,然后显示在前端页面,踢出对应用户就可以使用在对应sessionId的session域中设置key为kickout的值为true,上面的KickoutSessionControlFilter就会判断session域中的kickout值,做响应的处理。这里我就先不上代码了,大家可以自己试一试。之后再把代码同步到我的码云上,供大家学习交流。

 

Shiro 之 HashedCredentialsMatcher 认证匹配

Shiro 提供了用于加密密码和验证密码服务的 CredentialsMatcher 接口,而 HashedCredentialsMatcher 正是 CredentialsMatcher 的一个实现类。写项目的话,总归会用到用户密码的非对称加密,目前主流的非对称加密方式是 MD5 ,以及在 MD5 上的加盐处理,而 HashedCredentialsMatcher 也允许我们指定自己的算法和盐。本文将介绍 HashedCredentialsMatcher 的使用,以及对相关源码的进行解析,MD5 等加密知识请自行查阅。

HashedCredentialsMatcher 的使用
要使用 HashedCredentialsMatcher ,那么首先要进行配置。当然,前提是你已经引入了 Shiro 库。总共有三种配置方式:

XML 格式

<bean id="credentialsMatcher"
class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!-- 加密方式 -->
<property name="hashAlgorithmName" value="MD5" />
<!-- 加密次数 -->
<property name="hashIterations" value="2" />
<!-- 存储散列后的密码是否为16进制 -->
<property name="storedCredentialsHexEncoded" value="true" />
</bean>


ini 等配置文件

首先在 web.xml 中自定义 shiro.ini 位置

<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
<init-param>
<param-name>configPath</param-name>
<param-value>/WEB-INF/shiro.ini</param-value>
</init-param>
</filter>


然后除了 shiro 的通常配置之外,需加上:

credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
## 加密方式
credentialsMatcher.hashAlgorithmName=md5
## 加密次数
credentialsMatcher.hashIterations=2
## 存储散列后的密码是否为16进制
credentialsMatcher.storedCredentialsHexEncoded=true


建立 ShiroConfiguration 配置类,除了 shiro 的通常配置之外,需加上:

@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//加密方式
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//加密次数
hashedCredentialsMatcher.setHashIterations(2);
//存储散列后的密码是否为16进制
//hashedCredentialsMatcher.isStoredCredentialsHexEncoded();
return hashedCredentialsMatcher;
}

然后,在登录方法或者自定义的输入中获取登录 token,我选择的方式是在/login中获取:

public Object login(@RequestBody User userParam, HttpSession session) {
String name = userParam.getName();
name = HtmlUtils.htmlEscape(name);
Subject subject = SecurityUtils.getSubject();
// 生成token
UsernamePasswordToken token = new UsernamePasswordToken(name, userParam.getPassword());
try {
// 从自定义Realm获取安全数据进行验证
subject.login(token);
User user = userService.getByName(name);
session.setAttribute("user", user);
return Result.success();
} catch (AuthenticationException e) {
String message ="账号密码错误";
return Result.fail(message);
}
}

接下来,是自定义 Realm,之前我也有博文写过相关知识,所以只贴下代码作参考:

public class JPARealm extends AuthorizingRealm {

@Autowired
private UserService userService;

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 权限分配的相关知识在此不做介绍,重点在验证方面
SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
return s;
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName = token.getPrincipal().toString();
User user = userService.getByName(userName);
String passwordInDB = user.getPassword();
String salt = user.getSalt();
// 认证信息token里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名
// 盐也放进去
// 这样通过配置中的 HashedCredentialsMatcher 进行自动校验
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, passwordInDB, ByteSource.Util.bytes(salt),
getName());
return authenticationInfo;
}
}

HashedCredentialsMatcher 的源码分析
从开发者的角度来看,我们可以自己实现 CredentialsMatcher 的一个类来实现定制化的账户密码验证机制,例如:

public class MyCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = getCredentials(token);
Object accountCredentials = getCredentials(info);
return super.equals(tokenCredentials, accountCredentials);
}
}

SimpleCredentialsMatcher 是 CredentialsMatcher 这个类的默认实现,相信我,你基本上不会去自己实现 getCredentials 这种涉及到底层编码的方法,重点在于重写 doCredentialsMatch ,在这里你可以自定义账户密码验证机制。

不过实现 doCredentialsMatch 你还是有可能觉得麻烦,HashedCredentialsMatcher 封装好了 doCredentialsMatch() 方法,你可以完全不用管它。

上一节的使用中,流程就是:使用 token 类将用户输入的信息封装,然后采用 token 进行 login 操作。此时 shiro 将使用 token 中携带的用户信息调用 Realm 中自定义的 doGetAuthenticationInfo 方法进行校验比对,比对成功则登录成功。


原文链接:https://blog.csdn.net/zx48822821/article/details/84325504

 

springboot 整合shrio

shiroConfig

package com.hainei.config;


import com.hainei.shiro.CustomAccessControllerFilter;
import com.hainei.shiro.CustomHashedCredentialsMatcher;
import com.hainei.shiro.CustomRealm;
import com.hainei.shiro.RedisCacheManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.shiro.mgt.SecurityManager;

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

/**
 * Created with IntelliJ IDEA.
 * User: lzx
 * Date: 2020/02/14
 * Time: 10:34
 * Description: Shiro配置
 */
@Configuration
public class ShiroConfig {

    @Bean
    public RedisCacheManager redisCacheManager() {
        return new RedisCacheManager();
    }

    @Bean
    public CustomHashedCredentialsMatcher customHashedCredentialsMatcher() {
        return new CustomHashedCredentialsMatcher();
    }

    @Bean
    public CustomRealm customRealm() {
        CustomRealm customRealm = new CustomRealm();
        customRealm.setCredentialsMatcher(customHashedCredentialsMatcher());
        customRealm.setCacheManager(redisCacheManager());
        return customRealm;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(customRealm());
        return defaultWebSecurityManager;
    }

    /**
     * shiro过滤器,配置拦截哪些请求
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //自定义拦截器限制并发,参考博客:
        LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
        //用来校验token
        filtersMap.put("token", new CustomAccessControllerFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //配置不会被拦截的拦截 顺序判断
        filterChainDefinitionMap.put("/api/user/login", "anon");
        filterChainDefinitionMap.put("/api/user/token", "anon");
        //放开swagger-ui地址
        filterChainDefinitionMap.put("/doc.html ", "anon");
        filterChainDefinitionMap.put("/swagger/**", "anon");
        filterChainDefinitionMap.put("/v2/api-docs", "anon");
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/druid/**", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        filterChainDefinitionMap.put("/captcha.jpg", "anon");
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/csrf", "anon");
        filterChainDefinitionMap.put("/base/user/userExcelExport", "anon");
        filterChainDefinitionMap.put("/base/config/listAll", "anon");
        filterChainDefinitionMap.put("/base/config/getSysPicture", "anon");
        filterChainDefinitionMap.put("/base/config/saveActivationCode", "anon");
        filterChainDefinitionMap.put("/modeler.html", "anon");
        filterChainDefinitionMap.put("/wf/**", "anon");
        filterChainDefinitionMap.put("/editor-app/**", "anon");
        filterChainDefinitionMap.put("/chemical/checkCar/**", "anon");
        filterChainDefinitionMap.put("/PostCarInInfo", "anon");
        filterChainDefinitionMap.put("/WpSbxxController/exportSbxxTxm", "anon");
        filterChainDefinitionMap.put("/WpTzsbController/exportTzTxm", "anon");
        filterChainDefinitionMap.put("/PostPayDetail", "anon");
        filterChainDefinitionMap.put("/webservice/**", "anon");
        filterChainDefinitionMap.put("/chemical/checkCarInfo", "anon");
        filterChainDefinitionMap.put("/base/security/retrieve", "anon");
        filterChainDefinitionMap.put("/base/security/judgeLoginId", "anon");
        filterChainDefinitionMap.put("/base/security/judgeSecurity", "anon");
        filterChainDefinitionMap.put("/base/security/getSecurity", "anon");
        filterChainDefinitionMap.put("/people/online/add", "anon");
        filterChainDefinitionMap.put("/base/user/getUpdatePwdConfigVO", "anon");
        filterChainDefinitionMap.put("/**", "token,authc");
        filterChainDefinitionMap.put("/samp-websocket/**", "anon");
        //配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
        shiroFilterFactoryBean.setLoginUrl("/api/user/unLogin");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 开启shiro aop注解支持
     * 使用代理方式;所以需要开启代码支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

}
View Code

customAccessControllerFilter

package com.hainei.shiro;

import com.alibaba.fastjson.JSON;
import com.hainei.common.constants.BaseConstant;
import com.hainei.common.exception.BusinessException;
import com.hainei.common.exception.code.BaseResponseCode;
import com.hainei.common.utils.DataResult;
import com.hainei.service.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * Created with IntelliJ IDEA.
 * User: lzx
 * Date: 2020/02/13
 * Time: 18:04
 * Description: shiro认证拦截器-对所有接口进行拦截
 */
@Slf4j
public class CustomAccessControllerFilter extends AccessControlFilter {

    /**
     * 判断是否登录,登录后会走此方法,返回true,直接访问控制器
     * @param servletRequest
     * @param servletResponse
     * @param o
     * @return
     * @throws Exception
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        return false;
    }

    /**
     * 是否是拒绝登录,没有登录下走此方法
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        log.info(request.getMethod());
        log.info(request.getRequestURL().toString());
        //判断客户端是否携带accessToken

        try {
            String accessToken = request.getHeader(BaseConstant.ACCESS_TOKEN);
            if (StringUtils.isEmpty(accessToken)){
                throw new BusinessException(BaseResponseCode.TOKEN_NOT_NULL);
            }
            CustomUsernamePasswordToken customUsernamePasswordToken = new CustomUsernamePasswordToken(accessToken);
            getSubject(servletRequest,servletResponse).login(customUsernamePasswordToken);
        }catch (BusinessException e){
            customResponse(e.getCode(), e.getDefaultMessage(), servletResponse);
            return false;
        }catch (AuthenticationException e){
            if (e.getCause() instanceof BusinessException){
                BusinessException exception = (BusinessException) e.getCause();
                customResponse(exception.getCode(),exception.getDefaultMessage(),servletResponse);
            }else {
                customResponse(BaseResponseCode.SHIRO_AUTHENTICATION_ERROR.getCode()
                        ,BaseResponseCode.SHIRO_AUTHENTICATION_ERROR.getMsg(),servletResponse);
            }
            return false;
        }


        return true;
    }

    private void customResponse(int code,String msg,ServletResponse response){
        // 自定义异常的类,用户返回给客户端相应的JSON格式的信息
        try {
            DataResult result = DataResult.getResult(code, msg);
            response.setContentType("application/json; charset=utf-8");
            response.setCharacterEncoding("UTF-8");

            String userJson = JSON.toJSONString(result);
            ServletOutputStream out = response.getOutputStream();
            out.write(userJson.getBytes("UTF-8"));
            out.flush();
        }catch (IOException e){
            log.error("error={}", e);
        }

    }
}
View Code

customHashedCredentialsMatcher

package com.hainei.shiro;

import com.hainei.common.constants.BaseConstant;
import com.hainei.common.exception.BusinessException;
import com.hainei.common.exception.code.BaseResponseCode;
import com.hainei.common.token.JwtTokenUtil;
import com.hainei.service.RedisService;
import com.hainei.service.base.BaseConfigParamService;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.concurrent.TimeUnit;

/**
 * Created with IntelliJ IDEA.
 * User: lzx
 * Date: 2020/02/13
 * Time: 18:05
 * Description: 用于加密密码和验证密码服务
 */
public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {
    @Autowired
    private RedisService redisService;
    @Autowired
    private BaseConfigParamService configParamService;

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        CustomUsernamePasswordToken customUsernamePasswordToken = (CustomUsernamePasswordToken) token;
        String accessToken  = (String)customUsernamePasswordToken.getPrincipal();
        String userId = JwtTokenUtil.getUserId(accessToken);

        /**
         * 判断系统激活码是否有效
         */
        if (!BaseConstant.SUPER_ADMIN_ID.equals(userId)){
            configParamService.getAndValidActivationCode(userId);
        }
        /**
         * 判断用户是否被锁定
         */
        if (redisService.hasKey(BaseConstant.ACCOUNT_LOCK_KEY+userId)){
            throw new BusinessException(BaseResponseCode.ACCOUNT_LOCK);
        }
        /**
         * 判断用户是否被删除
         */
        if(redisService.hasKey(BaseConstant.DELETED_USER_KEY+userId)){
            throw new BusinessException(BaseResponseCode.ACCOUNT_HAS_DELETED_ERROR);
        }
        /**
         * 判断token 是否主动登出
         */
        if (redisService.hasKey(BaseConstant.JWT_ACCESS_TOKEN_BLACKLIST+accessToken)){
            throw new BusinessException(BaseResponseCode.TOKEN_ERROR);
        }
        /**
         * 判断token是否通过校验
         */
        if (!JwtTokenUtil.validateToken(accessToken)){
            throw new BusinessException(BaseResponseCode.TOKEN_PAST_DUE);
        }
        /**
         * 判断这个登录用户是否要主动去刷新
         *
         * 如果 key=BaseConstant.JWT_REFRESH_KEY+userId大于accessToken说明是在 accessToken不是重新生成的
         * 这样就要判断它是否刷新过了 / 或者是否是新生成的token
         */
//        if(redisService.hasKey(BaseConstant.JWT_REFRESH_KEY+userId)
//                &&redisService.getExpire(BaseConstant.JWT_REFRESH_KEY+userId, TimeUnit.MICROSECONDS)>JwtTokenUtil.getRemainingTime(accessToken)){
//            /**
//             * 是否存在刷新的标识
//             */
//            if(!redisService.hasKey(BaseConstant.JWT_REFRESH_IDENTIFICATION+accessToken)){
//                throw new BusinessException(BaseResponseCode.TOKEN_PAST_DUE);
//            }
//        }
        if (redisService.hasKey(BaseConstant.JWT_REFRESH_KEY+userId)){
            /**
             * 通过剩余的过期时间比较如果token的剩余过期时间大于标记key的剩余过期时间
             * 就说明这个token是在这个标记key之后生成的
             */
            if(redisService.getExpire(BaseConstant.JWT_REFRESH_KEY+userId,TimeUnit.MILLISECONDS)
                    >JwtTokenUtil.getRemainingTime(accessToken)){
                throw new BusinessException(BaseResponseCode.TOKEN_PAST_DUE);
            }
        }
        return true;
    }
}
View Code

customRealm

package com.hainei.shiro;

import com.hainei.common.constants.BaseConstant;
import com.hainei.common.enums.YesOrNo;
import com.hainei.common.token.JwtTokenUtil;
import com.hainei.service.RedisService;
import com.hainei.service.base.BasePermissionService;
import com.hainei.service.base.BaseRoleService;
import io.jsonwebtoken.Claims;
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.AuthorizationException;
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.context.annotation.Lazy;

import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Created with IntelliJ IDEA.
 * User: lzx
 * Date: 2020/02/13
 * Time: 18:05
 * Description: 将数据封装在Realm中  基础关系
 * CachingRealm(带缓存的realm)<--AutheticatingRealm(支持认证)<--AuthorizingRealm(支持授权)
 */
public class CustomRealm extends AuthorizingRealm {
    @Autowired
    private RedisService redisService;
//    @Autowired
//    private BaseRoleService roleService;
    @Autowired
    @Lazy
    private BasePermissionService permissionService;

    @Override
    public boolean isPermitted(PrincipalCollection principals, String permission) {
        String accessToken = (String)principals.getPrimaryPrincipal();
        String loginId = JwtTokenUtil.getLoginId(accessToken);
        return (loginId.equals(BaseConstant.SUPER_ADMIN_LOGIN_ID))||super.isPermitted(principals, permission);
    }

    @Override
    public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {
        String accessToken = (String)principal.getPrimaryPrincipal();
        String loginId = JwtTokenUtil.getLoginId(accessToken);
        return (loginId.equals(BaseConstant.SUPER_ADMIN_LOGIN_ID)) ||super.hasRole(principal, roleIdentifier);
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String accessToken = (String)principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        String userId = JwtTokenUtil.getUserId(accessToken);
        /**
         * 通过剩余的过期时间比较,如果token的剩余过期时间大于标记key的剩余过期时间
         * 就说明这个token是在这个标记key之后生成的
         */
        if (redisService.hasKey(BaseConstant.JWT_REFRESH_KEY+userId)
                &&redisService.getExpire(BaseConstant.JWT_REFRESH_KEY+userId, TimeUnit.MILLISECONDS)
                >JwtTokenUtil.getRemainingTime(accessToken)){
            if (userId.equals(BaseConstant.SUPER_ADMIN_ID)){
                info.addRole(BaseConstant.SUPER_ADMIN_ID);
            }
            //权限标识
            Set<String> permsByUserId = permissionService.listPermsByUserId(userId);
            if (permsByUserId!=null){
                info.addStringPermissions(permsByUserId);
            }
        }else {
//            Claims claims = JwtTokenUtil.getClaimsFromToken(accessToken);
            /**
             * 返回该用户的权限信息给授权器
             */
            Object o = redisService.get(BaseConstant.IDENTIFY_CACHE_KEY + userId);
            if (o!=null){
                info.addStringPermissions((Collection<String>)o);
            }
        }
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        CustomUsernamePasswordToken customUsernamePasswordToken = (CustomUsernamePasswordToken) authenticationToken;
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(customUsernamePasswordToken.getPrincipal()
                , customUsernamePasswordToken.getCredentials(), CustomRealm.class.getName());
        return info;
    }
}
View Code

customUsernamePasswordToken

package com.hainei.shiro;

import org.apache.shiro.authc.UsernamePasswordToken;

/**
 * Created with IntelliJ IDEA.
 * User: lzx
 * Date: 2020/02/13
 * Time: 18:05
 * Description: No Description
 */
public class CustomUsernamePasswordToken extends UsernamePasswordToken {

    private String token;

    public CustomUsernamePasswordToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }
}
View Code

 

jwtTokenUtil

package com.hainei.common.token;

import com.hainei.common.constants.BaseConstant;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import javax.xml.bind.DatatypeConverter;
import java.time.Duration;
import java.util.Date;
import java.util.Map;

/**
 * Created with IntelliJ IDEA.
 * User: lzx
 * Date: 2020/02/13
 * Time: 17:02
 * Description: token工具类
 */
@Slf4j
public class JwtTokenUtil {
    private static String secretKey;
    private static Duration accessTokenExpireTime;
    private static Duration refreshTokenExpireTime;
    private static Duration refreshTokenExpireAppTime;
    private static String issuer;
    private static Duration activationCodeExpireTime;

    public static void setTokenSettings(TokenSettings tokenSettings){
        secretKey=tokenSettings.getSecretKey();
        accessTokenExpireTime=tokenSettings.getAccessTokenExpireTime();
        refreshTokenExpireTime=tokenSettings.getRefreshTokenExpireTime();
        refreshTokenExpireAppTime=tokenSettings.getRefreshTokenExpireAppTime();
        issuer=tokenSettings.getIssuer();
        activationCodeExpireTime = tokenSettings.getActivationCodeExpireTime();
    }

    public static String getActivationCode(String subject,Map<String,Object> claims){
        return generateToken(issuer, subject, claims, activationCodeExpireTime.toMillis(), secretKey);
    }
    public static String getTokenByCustomExpireTime(String subject,Map<String,Object> claims,Duration customExpireTime){
        return generateToken(issuer, subject, claims, customExpireTime.toMillis(), secretKey);
    }
    /**
     * 生成 access_token
     * @Author:      小霍
     * @UpdateUser:
     * @Version:     0.0.1
     * @param subject
     * @param claims
     * @return       java.lang.String
     * @throws
     */
    public static String getAccessToken(String subject, Map<String,Object> claims){

        return generateToken(issuer,subject,claims,accessTokenExpireTime.toMillis(),secretKey);
    }
    /**
     * 签发token
     * @Author:      小霍
     * @UpdateUser:
     * @Version:     0.0.1
     * @param issuer 签发人
     * @param subject 代表这个JWT的主体,即它的所有人 一般是用户id
     * @param claims 存储在JWT里面的信息 一般放些用户的权限/角色信息
     * @param ttlMillis 有效时间(毫秒)
     * @return       java.lang.String
     * @throws
     */
    public static String generateToken(String issuer, String subject, Map<String, Object> claims, long ttlMillis, String secret) {

        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        byte[] signingKey = DatatypeConverter.parseBase64Binary(secret);

        JwtBuilder builder = Jwts.builder();
        if(null!=claims){
            builder.setClaims(claims);
        }
        if (!StringUtils.isEmpty(subject)) {
            builder.setSubject(subject);
        }
        if (!StringUtils.isEmpty(issuer)) {
            builder.setIssuer(issuer);
        }
        builder.setIssuedAt(now);
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }
        builder.signWith(signatureAlgorithm, signingKey);
        return builder.compact();
    }

    // 上面我们已经有生成 access_token 的方法,下面加入生成 refresh_token 的方法(PC 端过期时间短一些)

    /**
     * 生产 PC refresh_token
     * @Author:      小霍
     * @UpdateUser:
     * @Version:     0.0.1
     * @param subject
     * @param claims
     * @return       java.lang.String
     * @throws
     */
    public static String getRefreshToken(String subject,Map<String,Object> claims){
        return generateToken(issuer,subject,claims,refreshTokenExpireTime.toMillis(),secretKey);
    }

    /**
     * 生产 App端 refresh_token
     * @Author:      小霍
     * @UpdateUser:
     * @Version:     0.0.1
     * @param subject
     * @param claims
     * @return       java.lang.String
     * @throws
     */
    public static String getRefreshAppToken(String subject,Map<String,Object> claims){
        return generateToken(issuer,subject,claims,refreshTokenExpireAppTime.toMillis(),secretKey);
    }

    /**
     * 从令牌中获取数据声明
     * @Author:      小霍
     * @UpdateUser:
     * @Version:     0.0.1
     * @param token
     * @return       io.jsonwebtoken.Claims
     * @throws
     */
    public static Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(secretKey)).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 获取用户id
     * @Author:      小霍
     * @UpdateUser:
     * @Version:     0.0.1
     * @param token
     * @return       java.lang.String
     * @throws
     */
    public static String getUserId(String token){
        String userId=null;
        try {
            Claims claims = getClaimsFromToken(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            log.error("eror={}",e);
        }
        return userId;
    }


    /**
     * 获取登录账号
     */
    public static String getLoginId(String token){

        String loginId=null;
        try {
            Claims claims = getClaimsFromToken(token);
            loginId = (String) claims .get(BaseConstant.JWT_LOGIN_ID);
        } catch (Exception e) {
            log.error("eror={}",e);
        }
        return loginId;
    }

    /**
     * 获取登录角色
     */
    public static String getRoleId(String token){

        String roleId=null;
        try {
            Claims claims = getClaimsFromToken(token);
            roleId = (String) claims .get(BaseConstant.JWT_ROLE_ID);
        } catch (Exception e) {
            log.error("eror={}",e);
        }
        return roleId;
    }

    /**
     * 验证token 是否过期
     * @Author:      小霍
     * @UpdateUser:
     * @Version:     0.0.1
     * @param token
     * @param secretKey
     * @return       java.lang.Boolean
     * @throws
     */
    public static Boolean isTokenExpired(String token) {

        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            log.error("error={}",e);
            return true;
        }
    }
    /**
     * 校验令牌
     * @Author:      小霍
     * @UpdateUser:
     * @Version:     0.0.1
     * @param token
     * @return       java.lang.Boolean
     * @throws
     */
    public static Boolean validateToken(String token) {
        Claims claimsFromToken = getClaimsFromToken(token);
        return (null!=claimsFromToken && !isTokenExpired(token));
    }

    /**
     * 刷新token
     * @Author:      小霍
     * @UpdateUser:
     * @Version:     0.0.1
     * @param refreshToken
     * @param claims 主动去刷新的时候 改变JWT payload 内的信息
     * @return       java.lang.String
     * @throws
     */
    public static String refreshToken(String refreshToken,Map<String, Object> claims) {
        String refreshedToken;
        try {
            Claims parserclaims = getClaimsFromToken(refreshToken);
            /**
             * 刷新token的时候如果为空说明原先的 用户信息不变 所以就引用上个token里的内容
             */
            if(null==claims){
                claims=parserclaims;
            }
            refreshedToken = generateToken(parserclaims.getIssuer(),parserclaims.getSubject(),claims,accessTokenExpireTime.toMillis(),secretKey);
        } catch (Exception e) {
            refreshedToken = null;
            log.error("error={}",e);
        }
        return refreshedToken;
    }

    /**
     * 获取token的剩余过期时间
     * @Author:      小霍
     * @UpdateUser:
     * @Version:     0.0.1
     * @param token
     * @param secretKey
     * @return       long
     * @throws
     */
    public static long getRemainingTime(String token){
        long result=0;
        try {
            long nowMillis = System.currentTimeMillis();
            result= getClaimsFromToken(token).getExpiration().getTime()-nowMillis;
        } catch (Exception e) {
            log.error("error={}",e);
        }
        return result;
    }

    public static long getExpireTime(String token){
        long result=0;
        try {
            result= getClaimsFromToken(token).getExpiration().getTime();
        } catch (Exception e) {
            log.error("error={}",e);
        }
        return result;
    }
}
View Code

 

jwt:
  secretKey: 78944878877848fg)
  accessTokenExpireTime: PT2H
  refreshTokenExpireTime: PT8H
  refreshTokenExpireAppTime: P30D
  issuer: *****.com

 TokenSettings.java

package com.hainei.common.token;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.time.Duration;

/**
 * Created with IntelliJ IDEA.
 * User: lzx
 * Date: 2020/02/13
 * Time: 17:02
 * Description: 读取静态属性
 */
@Component
@Data
@ConfigurationProperties(prefix = "jwt")
public class TokenSettings {
    private String secretKey;
    private Duration accessTokenExpireTime;
    private Duration refreshTokenExpireTime;
    private Duration refreshTokenExpireAppTime;
    private String issuer;

    private Duration activationCodeExpireTime;
}
View Code

 

11

推荐阅读