大家好,我是程序员田同学。

公司开始了新我的项目,新我的项目的认证采纳的是Shiro实现。因为波及到多端登录用户,而且多端用户还是来自不同的表。

这就波及到了Shiro的多realm,明天的demo次要是介绍Shiro的多realm实现计划,文中蕴含所有的代码,须要的敌人能够无缝copy。

前后端拆散的背景下,在认证的实现中次要是两方面的内容,一个是用户登录获取到token,二是从申请头中拿到token并测验token的有效性和设置缓存。

1、用户登录获取token

登录和以往单realm实现逻辑一样,应用用户和明码生成token返回给前端,前端每次申请接口的时候携带token。

  @ApiOperation(value="登录", notes="登录")    public Result<JSONObject> wxappLogin(String username,String password){     Result<JSONObject> result = new Result<JSONObject>();        JSONObject obj = new JSONObject();    // 生成token    String password="0";    String token = JwtUtil.sign(username, password);        obj.put("token", token);        result.setResult(obj);        result.success("登录胜利");        return result;        }    

生成token的工具类

/** * 生成签名,5min后过期 * * @param username 用户名 * @param secret   用户的明码 * @return 加密的token */public static String sign(String username, String secret) {   Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);   Algorithm algorithm = Algorithm.HMAC256(secret);   // 附带username信息   return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);}

以上就实现了简略的登录逻辑,和Shiro的单realm设置和SpringSecurity的登录逻辑都没有什么区别。

2、鉴权登录拦截器(验证token有效性)

应用Shiro登录拦截器的只须要继承Shiro的 BasicHttpAuthenticationFilter 类 重写 isAccessAllowed()办法,在该办法中咱们从ServletRequest中获取到token和login_type。

须要特地指出的是,因为是多realm,咱们在申请头中退出一个login_type来辨别不同的登录类型。

通过token和login_type咱们生成一个JwtToken对象提交给getSubject。

JwtFilter过滤器

@Slf4jpublic class JwtFilter extends BasicHttpAuthenticationFilter {    /**     * 默认开启跨域设置(应用单体)     */    private boolean allowOrigin = true;    public JwtFilter(){}    public JwtFilter(boolean allowOrigin){        this.allowOrigin = allowOrigin;    }    /**     * 执行登录认证     *     * @param request     * @param response     * @param mappedValue     * @return     */    @Override    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {        try {            executeLogin(request, response);            return true;        } catch (Exception e) {            JwtUtil.responseError(response,401,CommonConstant.TOKEN_IS_INVALID_MSG);            return false;            //throw new AuthenticationException("Token生效,请从新登录", e);        }    }    /**     *     */    @Override    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {        HttpServletRequest httpServletRequest = (HttpServletRequest) request;        String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);        String loginType = httpServletRequest.getHeader(CommonConstant.LOGIN_TYPE);        // update-begin--Author:lvdandan Date:20210105 for:JT-355 OA聊天增加token验证,获取token参数        if (oConvertUtils.isEmpty(token)) {            token = httpServletRequest.getParameter("token");        }        // update-end--Author:lvdandan Date:20210105 for:JT-355 OA聊天增加token验证,获取token参数        JwtToken jwtToken = new JwtToken(token,loginType);        // 提交给realm进行登入,如果谬误他会抛出异样并被捕捉        getSubject(request, response).login(jwtToken);        // 如果没有抛出异样则代表登入胜利,返回true        return true;    }  }

JwtToken类

public class JwtToken implements AuthenticationToken {      private static final long serialVersionUID = 1L;   private String token;    private String loginType;//    public JwtToken(String token) {//        this.token = token;//    }    public JwtToken(String token,String loginType) {        this.token = token;        this.loginType=loginType;    }    public String getToken() {        return token;    }    public void setToken(String token) {        this.token = token;    }    public String getLoginType() {        return loginType;    }    public void setLoginType(String loginType) {        this.loginType = loginType;    }    @Override    public Object getPrincipal() {        return token;    }     @Override    public Object getCredentials() {        return token;    }}

再往下的逻辑必定会先依据咱们的login_type来走不同的realm了,而后在各自的realm中去查看token的有效性了,那Shiro怎么晓得咱们的Realm都是哪些呢?

接下来就该引出应用Shiro的外围配置文件了——ShiroConfig.java类

shiro的配置文件中会注入名字为securityManager的Bean。

在该bean中首先注入ModularRealmAuthenticator,ModularRealmAuthenticator会依据配置的AuthenticationStrategy(身份验证策略)进行多Realm认证过程。

因为是多realm咱们须要重写ModularRealmAuthenticator类,ModularRealmAuthenticator类中用于判断逻辑走不同的realm,接着注入咱们的两个realm,别离是myRealm和clientShiroRealm。

从新注入 ModularRealm类

 @Bean    public ModularRealm ModularRealm(){        //本人重写的ModularRealmAuthenticator        ModularRealm modularRealm = new ModularRealm();//        modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());//这里为默认策略:如果有一个或多个Realm验证胜利,所有的尝试都被认为是胜利的,如果没有一个验证胜利,则该次尝试失败        return modularRealm;    }

securityManager-bean。

 @Bean("securityManager")    public DefaultWebSecurityManager securityManager(ShiroRealm myRealm,    ClientShiroRealm clientShiroRealm,ModularRealm modularRealm) {        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();//        securityManager.setRealm(myRealm);        securityManager.setAuthenticator(modularRealm);        List<Realm> realms = new ArrayList<>();        //增加多个Realm        realms.add(myRealm);        realms.add(clientShiroRealm);        securityManager.setRealms(realms);        /*         * 敞开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);        securityManager.setSubjectDAO(subjectDAO);        //自定义缓存实现,应用redis        securityManager.setCacheManager(redisCacheManager());        return securityManager;    }

ModularRealm实现类

public class ModularRealm  extends ModularRealmAuthenticator {    @Override    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {        assertRealmsConfigured();        Collection<Realm> realms = getRealms();        // 登录类型对应的所有Realm        HashMap<String, Realm> realmHashMap = new HashMap<>(realms.size());        for (Realm realm : realms) {            // 这里应用的realm中定义的Name属性来进行辨别,留神realm中要加上            realmHashMap.put(realm.getName(), realm);        }        JwtToken token = (JwtToken) authenticationToken;        if (StrUtil.isEmpty(token.getLoginType())){            return  doSingleRealmAuthentication(realmHashMap.get(LoginType.DEFAULT.getType()),token);        } else {            return  doSingleRealmAuthentication(realmHashMap.get(token.getLoginType()),token);        }//        return super.doAuthenticate(authenticationToken);    }}

而后会依据不同的login_type到不同的realm,上面为我的Shiro认证realm。

myrealm类.

@Component@Slf4jpublic class ShiroRealm extends AuthorizingRealm {   @Lazy    @Resource    private CommonAPI commonApi;    @Lazy    @Resource    private RedisUtil redisUtil;    @Override    public String getName() {        return LoginType.DEFAULT.getType();    }    /**     * 必须重写此办法,不然Shiro会报错     */    @Override    public boolean supports(AuthenticationToken token) {//        return token instanceof JwtToken;        if (token instanceof JwtToken){            return StrUtil.isEmpty(((JwtToken) token).getLoginType()) || LoginType.CLIENT.getType().equals(((JwtToken) token).getLoginType());        } else {            return false;        }    }    /**     * 权限信息认证(包含角色以及权限)是用户拜访controller的时候才进行验证(redis存储的此处权限信息)     * 触发检测用户权限时才会调用此办法,例如checkRole,checkPermission     *     * @param principals 身份信息     * @return AuthorizationInfo 权限信息     */    @Override    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {        log.debug("===============Shiro权限认证开始============ [ roles、permissions]==========");        String username = null;        if (principals != null) {            LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();            username = sysUser.getUsername();        }        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();        // 设置用户领有的角色汇合,比方“admin,test”        Set<String> roleSet = commonApi.queryUserRoles(username);        System.out.println(roleSet.toString());        info.setRoles(roleSet);        // 设置用户领有的权限汇合,比方“sys:role:add,sys:user:add”        Set<String> permissionSet = commonApi.queryUserAuths(username);        info.addStringPermissions(permissionSet);        System.out.println(permissionSet);        log.info("===============Shiro权限认证胜利==============");        return info;    }    /**     * 用户信息认证是在用户进行登录的时候进行验证(不存redis)     * 也就是说验证用户输出的账号和明码是否正确,谬误抛出异样     *     * @param auth 用户登录的账号密码信息     * @return 返回封装了用户信息的 AuthenticationInfo 实例     * @throws AuthenticationException     */    @Override    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {        log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");        String token = (String) auth.getCredentials();        if (token == null) {            HttpServletRequest req = SpringContextUtils.getHttpServletRequest();            log.info("————————身份认证失败——————————IP地址:  "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());            throw new AuthenticationException("token为空!");        }        // 校验token有效性        LoginUser loginUser = null;        try {            loginUser = this.checkUserTokenIsEffect(token);        } catch (AuthenticationException e) {            JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());            e.printStackTrace();            return null;        }        return new SimpleAuthenticationInfo(loginUser, token, getName());    }    /**     * 校验token的有效性     *     * @param token     */    public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {        // 解密取得username,用于和数据库进行比照        String username = JwtUtil.getUsername(token);        if (username == null) {            throw new AuthenticationException("token非法有效!");        }        // 查问用户信息        log.debug("———校验token是否无效————checkUserTokenIsEffect——————— "+ token);        LoginUser loginUser = TokenUtils.getLoginUser(username,commonApi,redisUtil);        //LoginUser loginUser = commonApi.getUserByName(username);        if (loginUser == null) {            throw new AuthenticationException("用户不存在!");        }        // 判断用户状态        if (loginUser.getStatus() != 1) {            throw new AuthenticationException("账号已被锁定,请分割管理员!");        }        // 校验token是否超时生效 & 或者账号密码是否谬误        if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {            throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);        }        //update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否统一        String userTenantIds = loginUser.getRelTenantIds();        if(oConvertUtils.isNotEmpty(userTenantIds)){            String contextTenantId = TenantContext.getTenant();            String str ="0";            if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){                //update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断破绽                String[] arr = userTenantIds.split(",");                if(!oConvertUtils.isIn(contextTenantId, arr)){                    throw new AuthenticationException("用户租户信息变更,请从新登陆!");                }                //update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断破绽            }        }        //update-end-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否统一        return loginUser;    }    /**     * JWTToken刷新生命周期 (实现: 用户在线操作不掉线性能)     * 1、登录胜利后将用户的JWT生成的Token作为k、v存储到cache缓存外面(这时候k、v值一样),缓存有效期设置为Jwt无效工夫的2倍     * 2、当该用户再次申请时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证     * 3、当该用户这次申请jwt生成的token值曾经超时,但该token对应cache中的k还是存在,则示意该用户始终在操作只是JWT的token生效了,程序会给token对应的k映射的v值从新生成JWTToken并笼罩v值,该缓存生命周期从新计算     * 4、当该用户这次申请jwt在生成的token值曾经超时,并在cache中不存在对应的k,则示意该用户账户闲暇超时,返回用户信息已生效,请从新登录。     * 留神: 前端申请Header中设置Authorization放弃不变,校验有效性以缓存中的token为准。     *       用户过期工夫 = Jwt无效工夫 * 2。     *     * @param userName     * @param passWord     * @return     */    public boolean jwtTokenRefresh(String token, String userName, String passWord) {        String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));        if (oConvertUtils.isNotEmpty(cacheToken)) {            // 校验token有效性            if (!JwtUtil.verify(cacheToken, userName, passWord)) {                //生成token                String newAuthorization = JwtUtil.sign(userName, passWord);                // 设置超时工夫                redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);                redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);                log.debug("——————————用户在线操作,更新token保障不掉线—————————jwtTokenRefresh——————— "+ token);            }            //update-begin--Author:scott  Date:20191005  for:解决每次申请,都重写redis中 token缓存问题//       else {//          // 设置超时工夫//          redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);//          redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);//       }            //update-end--Author:scott  Date:20191005   for:解决每次申请,都重写redis中 token缓存问题            return true;        }        //redis中不存在此TOEKN,阐明token非法返回false        return false;    }    /**     * 革除以后用户的权限认证缓存     *     * @param principals 权限信息     */    @Override    public void clearCache(PrincipalCollection principals) {        super.clearCache(principals);    }}

ClientShiroRealm类.

@Component@Slf4jpublic class ClientShiroRealm extends AuthorizingRealm {   @Lazy    @Resource    private ClientAPI clientAPI;    @Lazy    @Resource    private RedisUtil redisUtil;    @Override    public String getName() {        return LoginType.CLIENT.getType();    }    /**     * 必须重写此办法,不然Shiro会报错     */    @Override    public boolean supports(AuthenticationToken token) {//        return token instanceof JwtToken;        if (token instanceof JwtToken){            return  LoginType.CLIENT.getType().equals(((JwtToken) token).getLoginType());        } else {            return false;        }    }    /**     * 权限信息认证(包含角色以及权限)是用户拜访controller的时候才进行验证(redis存储的此处权限信息)     * 触发检测用户权限时才会调用此办法,例如checkRole,checkPermission     *     * @param principals 身份信息     * @return AuthorizationInfo 权限信息     */    @Override    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {        log.debug("===============Shiro权限认证开始============ [ roles、permissions]==========");        //String username = null;        //if (principals != null) {        //    LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();        //    username = sysUser.getUsername();        //}        //SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();        //// 设置用户领有的角色汇合,比方“admin,test”        //Set<String> roleSet = commonApi.queryUserRoles(username);        //System.out.println(roleSet.toString());        //info.setRoles(roleSet);        //        //// 设置用户领有的权限汇合,比方“sys:role:add,sys:user:add”        //Set<String> permissionSet = commonApi.queryUserAuths(username);        //info.addStringPermissions(permissionSet);        //System.out.println(permissionSet);        log.info("===============Shiro权限认证胜利==============");        return null;    }    /**     * 用户信息认证是在用户进行登录的时候进行验证(不存redis)     * 也就是说验证用户输出的账号和明码是否正确,谬误抛出异样     *     * @param auth 用户登录的账号密码信息     * @return 返回封装了用户信息的 AuthenticationInfo 实例     * @throws AuthenticationException     */    @Override    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {        log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");        String token = (String) auth.getCredentials();        if (token == null) {            HttpServletRequest req = SpringContextUtils.getHttpServletRequest();            log.info("————————身份认证失败——————————IP地址:  "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());            throw new AuthenticationException("token为空!");        }        // 校验token有效性        LoginUser loginUser = null;        try {            loginUser = this.checkUserTokenIsEffect(token);        } catch (AuthenticationException e) {            JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());            e.printStackTrace();            return null;        }        return new SimpleAuthenticationInfo(loginUser, token, getName());    }    /**     * 校验token的有效性     *     * @param token     */    public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {        // 解密取得username,用于和数据库进行比照        String username = JwtUtil.getUsername(token);        if (username == null) {            throw new AuthenticationException("token非法有效!");        }        // 查问用户信息        log.debug("———校验token是否无效————checkUserTokenIsEffect——————— "+ token);        LoginUser loginUser = TokenUtils.getClientLoginUser(username,clientAPI,redisUtil);        //LoginUser loginUser = commonApi.getUserByName(username);        if (loginUser == null) {            throw new AuthenticationException("用户不存在!");        }        // 校验token是否超时生效 & 或者账号密码是否谬误        if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {            throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);        }        return loginUser;    }    /**     * JWTToken刷新生命周期 (实现: 用户在线操作不掉线性能)     * 1、登录胜利后将用户的JWT生成的Token作为k、v存储到cache缓存外面(这时候k、v值一样),缓存有效期设置为Jwt无效工夫的2倍     * 2、当该用户再次申请时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证     * 3、当该用户这次申请jwt生成的token值曾经超时,但该token对应cache中的k还是存在,则示意该用户始终在操作只是JWT的token生效了,程序会给token对应的k映射的v值从新生成JWTToken并笼罩v值,该缓存生命周期从新计算     * 4、当该用户这次申请jwt在生成的token值曾经超时,并在cache中不存在对应的k,则示意该用户账户闲暇超时,返回用户信息已生效,请从新登录。     * 留神: 前端申请Header中设置Authorization放弃不变,校验有效性以缓存中的token为准。     *       用户过期工夫 = Jwt无效工夫 * 2。     *     * @param userName     * @param passWord     * @return     */    public boolean jwtTokenRefresh(String token, String userName, String passWord) {        String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));        if (oConvertUtils.isNotEmpty(cacheToken)) {            // 校验token有效性            if (!JwtUtil.verify(cacheToken, userName, passWord)) {                //生成token                String newAuthorization = JwtUtil.sign(userName, passWord);                // 设置超时工夫                redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);                redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);                log.debug("——————————用户在线操作,更新token保障不掉线—————————jwtTokenRefresh——————— "+ token);            }            return true;        }        //redis中不存在此TOEKN,阐明token非法返回false        return false;    }    /**     * 革除以后用户的权限认证缓存     *     * @param principals 权限信息     */    @Override    public void clearCache(PrincipalCollection principals) {        super.clearCache(principals);    }}

这两个realm更多的是须要实现咱们本身的realm,我把我的全副代码贴上,读者可依据本人的须要进行批改,两个办法大抵的作用都是测验token的有效性,只是查问的用户从不同的用户表中查出来的。

至此,Shiro的多Realm实现计划到这里就正式完结了。