Shiro整合SSO单点登录系统

1 篇文章 0 订阅
0 篇文章 0 订阅
本文介绍了一位工程师如何从零开始,基于Shiro框架,对公司内部的单点登录系统进行改造的过程。通过深入研究Shiro的工作原理,以及借鉴网上的资源,作者实现了与公司现有系统的无缝对接,特别是在密码验证逻辑上的巧妙设计,使得单点登录得以在不破坏原有代码逻辑的基础上实现。
摘要由CSDN通过智能技术生成

本文的单点登录不是cas的,是公司自己做的单点登录系统,主要就是发送特定地址一个带时效的token,特定的方法进行解析处理等,解析好的用户名在当前系统数据库中查询用户相关信息是否存在,包括用户名,密码(SHA1加密不可逆),salt(盐)等信息,如果能查询到该用户,则说明单点登录成功并且在当前系统成功有权避开用户名密码的填写,完成系统的单点登录。

第一次接触shiro框架代理的登录,让我去改造成单点登录,一脸懵逼,然后没办法硬着头皮去做,因为不研究这个系统,根据具体情况,谁也不知道怎么改,别人也没有这个时间精力去管你,所以只能靠自己。

第一个阶段研究了下shiro框架具体流程,具体参照:https://blog.csdn.net/YANGYU1079075086/article/details/78969983?utm_source=blogxgwz0

我参考了网上大部分文章,就觉得他写的最好最透彻也是我这种菜鸟最容易看懂的,边结合代码看,边去按照他写的去代码中找,理解了shiro是怎么控制登录流程的。哪怕看不懂,大致能有个思路,就是这个东西看着眼熟就够了,晚上睡前再想一想。

然后第二个阶段研究了下网上的Shiro整合SSO单点登录系统,具体参照:https://blog.csdn.net/m0_37797991/article/details/78529096

1.虽然大神写的很好,但是并不适用于我的系统,值得庆幸的是他的代码中写了如何避开加盐加密的方法,让我起了反思,成功的解决了这部分问题,也是利用常量,单点登录的写在if里面,else里面是系统原来的密码用户名校验逻辑,我并不想破坏它,那问题来了,如何判断这个请求是来自于哪里的呢,我的设计思路是将密码变成常量,“password”-“salt”的组合,然后在if判断一下是不是这个结构,是不是当前这个用户名对应的加盐加密数值。

2.首先就将单点登录的密码改造为常量

3

那么问题来了,如何单点登录,并且不破环系统原有的代码逻辑,我思考了两种方案。

第一种就是放开一个通道,过滤器放开要解析token的Controller方法,然后重定向到登录页面后台发请求模拟登录,当然估计是忙傻了,这种怎么可能好使,因为我系统之前是有过滤器的,就造成了,哪怕我有session也没有跳到我想要的画面,又被系统之前的过滤器拦截了,走输密码登录的那些逻辑了,这样它造成死循环了,还以为就差临门一脚呢,结果一细想,白忙活了。

第二种我想了一下,因为是被拦截我就想能不能在我们之前登录一系列逻辑加过滤器的前面加一个过滤器,在里面实现登录以后该有的session还shiroUser什么的,这样继续走余下的过滤器,它系统机制肯定能通过校验成功登录到后台。完全有可能,想到这里兴奋的搓了搓手,开动。

4.想是这么想但是还是不会怎么改配置文件呢?Shiro.xml,一看就脑袋疼,哎呀妈呀脑瓜疼,一看到配置文件就脑瓜疼,疼也得看啊,就看,参考上面那个大神的配置的代码,我发现写的是挺好的,但是吧,我这个配置文件中自己原来就有个自定义过滤器,也有对应的realm,本来我也想再加一个自定义过滤器,加个realm的,自定义过滤器加成功了,但是realm说死不走我自己的。 

就是这样配的,后来发现其实不加realm也行。走过滤器就行,创建好session后它会继续走之前项目realm中的该有的那些逻辑。

所以不加也行。

然后还参考了下别人写的那个Shiro+cas单点登录的配置文件,最好生成了我自己的配置文件。

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd



    http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <!-- see: http://fivefish.iteye.com/blog/962457  -->
    <description>Shiro 配置</description>

    <bean id="sessionFactory" class="com.xxx.activiti.base.shiro.SessionRecordFactory">
    </bean>

    <bean id="shiroActiveSessionCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
        <!-- <property name="blocking" value="true" /> -->
        <property name="cacheManager">
            <ref bean="ehCacheManagerFactory" />
        </property>
        <property name="cacheName">
            <value>shiroActiveSessionCache</value>
        </property>
    </bean>

    <bean id="sessionDao" class="com.xxx.activiti.base.dao.SessionDaoImpl">
        <property name="sessionCache" ref="shiroActiveSessionCache"/>
        <property name="sessionRecordDao" ref="sessionRecordDao"/>
    </bean>

    <bean id="nativeSessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <property name="globalSessionTimeout" value="7200000"/>
        <property name="sessionFactory" ref="sessionFactory"/>
        <property name="sessionDAO" ref="sessionDao"/>
    </bean>

    <!-- Shiro's main business-tier object for web-enabled applications -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref ="shiroDbRealm" />
        <property name="cacheManager" ref="shiroEhcacheManager" />
        <property name="sessionManager" ref="nativeSessionManager" />
    </bean>

    <!-- 自定义的Realm数据源 -->
    <bean id="shiroDbRealm" class="com.xxx.activiti.base.shiro.ShiroDbRealm"
          depends-on="operationLogDao,commonConfigDao,userDao,sessionRecordDao,userService,commonConfigService">
        <property name="authorizationCacheName" value="shiroAuthorizationCache" />
    </bean>

    <bean id="shiroFilter" class="com.xxx.base.shiro.ShiroFilterFactoryBean">
        <!-- shiro的核心安全接口 -->
        <property name="securityManager" ref="securityManager" />
        <property name="filters">
            <util:map>
                <entry key="ssoFilter" value-ref="ssoFilter"/><!-- 自定义的ssoFilter要放到最前边 -->
                <entry key="authc" value-ref="formAuthenticationFilter" />
                <entry key="user" value-ref="userFilter" />
                <entry key="logout" value-ref="logoutFilter" />
            </util:map>
        </property>
        <!-- 要求登录时的链接 -->
        <property name="loginUrl" value="/login/" />
        <!-- 登录成功后要跳转的连接 -->
        <property name="successUrl" value="/" />
        <!-- 未授权时要跳转的连接 -->
        <property name="unauthorizedUrl" value="/no_permission" />
        <!-- shiro连接约束配置 -->
        <property name="filterChainDefinitions">
            <!-- anon 匿名过滤器 authc 如果继续操作,需要做对应的表单验证否则不能通过 authcBasic 基本http验证过滤,如果不通过,跳转屋登录页面
                logout 登录退出过滤器 noSessionCreation 没有session创建过滤器 perms 权限过滤器 port 端口过滤器,可以设置是否是指定端口如果不是跳转到登录页面
                rest http方法过滤器,可以指定如post不能进行访问等 roles 角色过滤器,判断当前用户是否指定角色 ssl 请求需要通过ssl,如果不是跳转回登录页
                user 如果访问一个已知用户,比如记住我功能,走这个过滤器 -->

            <value>
                /jwt/sso/login = ssoFilter <!--自定义的过滤器-->
                /login = anon
                /login/ = authc
                /logout/ = logout
                /s/** = anon
                /i18n/**=anon
                /robots.txt = anon
                /400 =anon
                /404 =anon
                /500 =anon
                /reset =anon
                /change =anon
                /error/exception/=anon
                /** = user
                /model/** = anon
                *.html = anon
            </value>
        </property>
    </bean>

    <!-- shiro 的缓存管理,使用spring的ehCacheManagerFactory, 操作spring缓存工厂来及时更新shiro的缓存管理
        ehCacheManagerFactory对象在applicationContext.xml中配置 -->
    <bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManager" ref="ehCacheManagerFactory" />
    </bean>

    <!-- shiro为集成spring -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />

    <!-- shiro增加验证码支持 -->
    <bean id="ssoFilter" class="com.xxx.activiti.base.shiro.SSOFilter" />
    <bean id="formAuthenticationFilter" class="com.xxx.activiti.base.shiro.CaptchaFormAuthenticationFilter"/>
    <bean id="logoutFilter" class="com.xxx.activiti.base.shiro.LogoutFilter" />
    <bean id="userFilter" class="com.xxx.activiti.base.shiro.UserFilter" />


</beans>
package com.xxx.activiti.base.shiro;

import com.idsmanager.dingdang.jwt.DingdangUserRetriever;

import com.xxx.activiti.entity.user.User;

import com.xxx.activiti.user.service.UserService;

import com.xxx.base.ClientInfoHolder;

import com.xxx.base.model.ClientInfo;

import org.apache.shiro.SecurityUtils;

import org.apache.shiro.web.util.WebUtils;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.web.servlet.i18n.CookieLocaleResolver;

import javax.annotation.Resource;

import javax.servlet.*;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

import java.util.Locale;

public class SSOFilter implements Filter {
@Autowired
private UserService userService;
@Resource
private ShiroDbRealm shiroDbRealm;
@Autowired
private ShiroUserService shiroUserService;
@Value("${sso.public.key}")
private String publicKey;
public static final String KEY_PARAM_CAPTCHA = "kaptcha_login";
protected final transient Logger logger = LoggerFactory.getLogger(getClass());

    private boolean isExistedUsername(String userName) {

        User user = userService.findByItalentId(userName);

        if(user != null) {

            return true;

        }

        return false;

    }

    @Override

    public void init(FilterConfig filterConfig) throws ServletException {

       

    }

    @Override

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        ShiroUser su = (ShiroUser) SecurityUtils.getSubject().getPrincipal();

        // 如果用户已处于登录状态则redirect到根目录

        if (su != null && su.getUserId().longValue() > 0) {

            request.setAttribute("success","成功");

            chain.doFilter(request,response);

        }else {

            HttpServletRequest httpRequests = (HttpServletRequest) request;

            HttpServletResponse httpResponses = (HttpServletResponse)response;

            DingdangUserRetriever.User user = null;//整个try块中的内容就是解析token,可以用自己公司的代码替换掉

            try {

                String token = httpRequests.getParameter("id_token");

                DingdangUserRetriever retriever = new DingdangUserRetriever(token, publicKey);

                //2.获取用户信息

                user = retriever.retrieve();

                if (user == null) {

                    httpResponses.sendRedirect("/");

                    return ;

                }

            } catch (Exception e) {

                httpResponses.sendRedirect("/");

                return ;

            }

            //3.判断用户名是否在自己系统存在isExistedUsername()方法为业务系统自行判断数据库中是否存在

        if (isExistedUsername(user.getSub())) {

                User userDetail = userService.findByItalentId(user.getSub());

                //以下内容是看项目源码中对比未登录前与登录过程中的代码中的值进行new与赋值的。

                //部分没有什么用不过不加会判空校验过不去,比如说那个什么电脑客户端的什么信息ClientInfo的那些

                String languageStr = WebUtils.getCleanParam(request, "language");

                if (languageStr == null || languageStr.length() == 0) {

                    languageStr = "zh_CN";

                }

                String[] languageArr = languageStr.split("_");

                Locale language = null;

                if (languageArr.length == 1) {

                    language = new Locale(languageArr[0]);

                } else {

                    language = new Locale(languageArr[0], languageArr[1]);

                }

                CookieLocaleResolver localeResolver = new CookieLocaleResolver();

                localeResolver.setCookieName("locale");

                localeResolver.setLocale((HttpServletRequest) request,

                                                (HttpServletResponse) response, language);

                //很有用的代码,就是创建具体是哪个用户登录的ShiroUser相关信息。密码用加密过的加上加盐

                //的信息组成的常量。到后面CaptchaFormAuthenticationFilter过滤器和userFilter过滤器可以顺利通过。

                CaptchaFormAuthenticationFilter.UsernamePasswordCaptchaToken t = new CaptchaFormAuthenticationFilter.UsernamePasswordCaptchaToken(userDetail.getUsername(), userDetail.getPassword() + "-" + userDetail.getSalt(), false,

                                                request.getRemoteHost(), WebUtils.getCleanParam(request, KEY_PARAM_CAPTCHA), "", "/", request.getServerName(), language);

                ClientInfo clientInfo = new ClientInfo();

                clientInfo.setSessionId(((HttpServletRequest) request).getRequestedSessionId());

                clientInfo.setPyid(((HttpServletRequest) request).getSession().getId());

                clientInfo.setIp(request.getRemoteHost());

                clientInfo.setReferrerUrl("http://localhost:8080/");

                clientInfo.setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6");

                ClientInfoHolder.setClientInfo(clientInfo);

                SecurityUtils.getSubject().login(t);

                //这段也很有用,暗搓搓想不加来的,后来发现不好使,也不知道是干什么的。

                if (t.getNext() != null) {

                    WebUtils.issueRedirect(request, response, t.getNext(), null, false);

                }

                request.setAttribute("success","成功");

                chain.doFilter(request, response);

            }else {

                httpResponses.sendRedirect("/");

                return;

            }

        }

    }

    @Override

    public void destroy() {
      
    }
}

 

 

/**

 * context根路径controller

 */

@Controller

@RequestMapping(value = "/")

public class RootController {

    @Autowired

    private UserService userService;

    

    @Resource

    private ShiroDbRealm shiroDbRealm;

    @RequestMapping(value = "jwt/sso/login" ,method = RequestMethod.GET)

    public String ssoUrl(@RequestParam(required = false) String id_token, String redirect_url, Model model, HttpServletRequest request) {

        Object abc = request.getAttribute("success");

        if(abc!= null) {

            model.addAttribute("success", "成功");

        }else {

            model.addAttribute("error", "失败");

        }

        return "index/index";

    }

}
 

 

 

//这个CaptchaFormAuthenticationFilter是我们系统原有的在登录前需要走的过滤器。

public class CaptchaFormAuthenticationFilter extends FormAuthenticationFilter {

    protected final transient Logger logger = LoggerFactory

            .getLogger(getClass());

    private static String encodeUrlParam(String str) {

        try {

            return URLEncoder.encode(str, "UTF-8");

        } catch (UnsupportedEncodingException e) {

            // 忽略

            return null;

        }

    }

    public static final String KEY_PARAM_CAPTCHA = "kaptcha_login";

    private String getCaptcha(ServletRequest request) {

        return WebUtils.getCleanParam(request, KEY_PARAM_CAPTCHA);

    }

 

    

    @Override

    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,

            ServletResponse response) throws Exception {

        UsernamePasswordCaptchaToken t = (UsernamePasswordCaptchaToken) token;

        if (t.getNext() != null) {

            WebUtils.issueRedirect(request, response, t.getNext(), null, false);

            return false;

        }

        return super.onLoginSuccess(token, subject, request, response);

    }

    @Override

    protected AuthenticationToken createToken(ServletRequest request,

            ServletResponse response) {

        String username = getUsername(request);

        String password = getPassword(request);

        boolean rememberMe = isRememberMe(request);

        String host = getHost(request);

        String service = WebUtils.getCleanParam(request, "service");

        String next = WebUtils.getCleanParam(request, "next");

        String captcha = getCaptcha(request);

        String domain = request.getServerName();

        String languageStr = WebUtils.getCleanParam(request, "language");

        if (languageStr == null || languageStr.length() == 0) {

            languageStr = "zh_CN";

        }

        String[] languageArr = languageStr.split("_");

        Locale language = null;

        if (languageArr.length == 1) {

            language = new Locale(languageArr[0]);

        } else {

            language = new Locale(languageArr[0], languageArr[1]);

        }

        CookieLocaleResolver localeResolver = new CookieLocaleResolver();

        localeResolver.setCookieName("locale");

        localeResolver.setLocale((HttpServletRequest) request,

                (HttpServletResponse) response, language);

        return new UsernamePasswordCaptchaToken(username, password, rememberMe,

                host, captcha, service, next, domain, language);

    }

    @Override

    protected boolean onAccessDenied(ServletRequest request,

            ServletResponse response) throws Exception {

        if (isLoginRequest(request, response)) {

            if (isLoginSubmission(request, response)) {

                return executeLogin(request, response);

            } else {

                return true;

            }

        } else {

            HttpServletRequest req = WebUtils.toHttp(request);

            StringBuilder loginUrl = new StringBuilder(getLoginUrl());

            if (StringUtils.isNotBlank(req.getRequestURI())) {

                loginUrl.append("?");

                loginUrl.append(UserFilter.PARAM_NEXT);

                loginUrl.append("=");

                loginUrl.append(URLEncoder.encode(req.getRequestURI(), "UTF-8"));

                if (StringUtils.isNotEmpty(req.getQueryString())) {

                    loginUrl.append(URLEncoder.encode(

                            "?" + req.getQueryString(), "UTF-8"));

                }

            }

            WebUtils.issueRedirect(request, response, loginUrl.toString());

            return false;

        }

    }

    public static class UsernamePasswordCaptchaToken extends

            UsernamePasswordToken {

        private static final long serialVersionUID = 3715989977528807192L;

        private String captcha;

        private String next;

        private String service;

        private String domain;

        private boolean thirdLogin;

        private Locale language;

        private String dmptoken;

        

        private String thirdtoken;

        public void setThirdtoken(String thirdtoken) {

            this.thirdtoken = thirdtoken;

        }

        

        public String getThirdtoken() {

            return thirdtoken;

        }

        public String getCaptcha() {

            return captcha;

        }

        public String getDomain() {

            return domain;

        }

        public String getNext() {

            return next;

        }

        public String getService() {

            return service;

        }

        public String getDmptoken() {

            return dmptoken;

        }

        public boolean isThirdLogin() {

            return this.thirdLogin;

        }

        public Locale getLanguage() {

            return language;

        }

        public void setLanguage(Locale language) {

            this.language = language;

        }

        public UsernamePasswordCaptchaToken(String username, String password,

                boolean rememberMe, String host, String captcha,

                String service, String next, String domain, Locale language) {

            super(username, password != null ? password.toCharArray() : null,

                    rememberMe, host);

            this.next = next;

            this.service = service;

            this.captcha = captcha;

            this.domain = domain;

            this.language = language;

        }

    @Override

    protected void setFailureAttribute(ServletRequest request,

            AuthenticationException ae) {

        logger.warn("登录过程中出现异常:", ae);

        request.setAttribute(getFailureKeyAttribute(), ae.getLocalizedMessage());

    }

    public static void main(String[] args) {

     

    }

}

 

 

package com.xxx.activiti.base.shiro;

import java.io.Serializable;

import java.sql.Timestamp;

import java.util.Calendar;

import java.util.Collection;

import java.util.Collections;

import java.util.Date;

import java.util.LinkedHashSet;

import java.util.List;

import java.util.Set;

import javax.annotation.PostConstruct;

import org.apache.commons.codec.DecoderException;

import org.apache.commons.codec.binary.Hex;

import org.apache.commons.collections.CollectionUtils;

import org.apache.commons.lang3.StringUtils;

import org.apache.commons.lang3.exception.ExceptionUtils;

import org.apache.shiro.SecurityUtils;

import org.apache.shiro.authc.AccountException;

import org.apache.shiro.authc.AuthenticationException;

import org.apache.shiro.authc.AuthenticationInfo;

import org.apache.shiro.authc.AuthenticationToken;

import org.apache.shiro.authc.CredentialsException;

import org.apache.shiro.authc.DisabledAccountException;

import org.apache.shiro.authc.IncorrectCredentialsException;

import org.apache.shiro.authc.LockedAccountException;

import org.apache.shiro.authc.SimpleAuthenticationInfo;

import org.apache.shiro.authc.UnknownAccountException;

import org.apache.shiro.authc.credential.CredentialsMatcher;

import org.apache.shiro.authz.AuthorizationInfo;

import org.apache.shiro.authz.Permission;

import org.apache.shiro.authz.SimpleAuthorizationInfo;

import org.apache.shiro.authz.permission.PermissionResolver;

import org.apache.shiro.cache.Cache;

import org.apache.shiro.realm.AuthorizingRealm;

import org.apache.shiro.subject.PrincipalCollection;

import org.apache.shiro.subject.SimplePrincipalCollection;

import org.apache.shiro.util.ByteSource;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.PageRequest;

import org.springframework.data.domain.Sort.Direction;

import com.xxx.activiti.base.autoreload.ReloadableConfig;

import com.xxx.activiti.base.entity.SessionRecord;

import com.xxx.activiti.base.exception.IncorrectCaptchaException;

import com.xxx.activiti.base.exception.LoginConcurrentLimitException;

import com.xxx.activiti.base.shiro.CaptchaFormAuthenticationFilter.UsernamePasswordCaptchaToken;

import com.xxx.activiti.entity.user.User;

import com.xxx.base.ClientInfoHolder;

import com.xxx.base.util.I18nResourceUtil;

import org.springframework.web.servlet.ThemeResolver;

public class ShiroDbRealm extends AuthorizingRealm {

    /**

     * 用户名不能为空

     */

    public static final String  ERROR_420 ="420";

    /**

     * 密码必填

     */

    public static final String  ERROR_421 ="421";

    /**

     * 用户不存在

     */

    public static final String  ERROR_422 ="422";

    /**

     * 请输入验证码

     */

    public static final String  ERROR_423 ="423";

    /**

     * 该IPxx小时内登录错误已超过xx次, 该IP将被锁定xx小时

     */

    public static final String  ERROR_424 ="424";

    /**

     * 该账号xx小时内登录错误已超过xx次, 该账号将被锁定xx小时

     */

    public static final String  ERROR_425 ="425";

    /**

     * 验证码错误

     */

    public static final String  ERROR_426 ="426";

    /**

     * 密码错误

     */

    public static final String  ERROR_427 ="427";

    /**

     * 用户尚未审核通过

     */

    public static final String  ERROR_428 ="428";

    /**

     * 用户同时登录了xx次,已达到最大限制

     */

    public static final String  ERROR_429 ="429";

    /**

     * 该用户暂时不可用,请联系客服,谢谢

     */

    public static final String  ERROR_430 ="430";

    /**

     * 当前登录用户不拥有任何广告主

     */

    public static final String  ERROR_431 ="431";

    /**

     * 当前登录用户不拥有任何客户

     */

    public static final String  ERROR_437 ="437";

    /**

     * 密码已经三个月未修改,请修改后登录

     */

    public static final String  ERROR_432 ="432";

    /**

     * 用户已被锁定

     */

    public static final String  ERROR_433 ="433";

    /**

     * 用户已被屏蔽

     */

    public static final String  ERROR_434 ="434";

    /**

     * 系统未知错误,请联系管理员

     */

    public static final String  ERROR_435 ="435";

    /**

     * 用户未登录邮箱激活

     */

    public static final String ERROR_436 = "436";

    protected final transient Logger logger = LoggerFactory

            .getLogger(getClass());

    @Autowired

    private ShiroUserService shiroUserService;

    /**

     * 设定Password校验的Hash算法与迭代次数.

     */

    @PostConstruct

    public void initCredentialsMatcher() {

        /*

         * HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(

         * UserService.HASH_ALGORITHM);

         */

        // CustomCredentialsMatcher matcher = new CustomCredentialsMatcher();

        setCredentialsMatcher(new AlwaysPassCredentialsMatcher());

        setName(SessionRecord.REALM_NAME_WEB_FORM);

    }

    /**

     * 获取当前登录用户的权限

     */

    @Override

    protected AuthorizationInfo doGetAuthorizationInfo(

            PrincipalCollection principals) {

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal();

        Set<String> permissions = shiroUserService.getAllPermission(shiroUser

                .getAccount());

        if (!shiroUser.getAccount().equals(shiroUser.getRealAccount())

                && shiroUser.isRemainPerm()) {

            permissions.addAll(shiroUserService.getAllPermission(shiroUser

                    .getRealAccount()));

        }

        info.addObjectPermissions(resolvePermissions(permissions));

        // info.addStringPermissions(permissions);

        logger.debug("{}的权限清单为:{}", shiroUser.getAccount(),

                StringUtils.join(permissions, ", "));

        return info;

    }

    private Collection<Permission> resolvePermissions(Collection<String> stringPerms) {

        Collection<Permission> perms = Collections.emptySet();

        PermissionResolver resolver = getPermissionResolver();

        if (resolver != null && !CollectionUtils.isEmpty(stringPerms)) {

            perms = new LinkedHashSet<Permission>(stringPerms.size());

            for (String strPermission : stringPerms) {

                Permission permission = resolver.resolvePermission(strPermission);

                perms.add(permission);

            }

        }

        return perms;

    }

    /**

     * 用户通过客户端登录时,进行各种业务逻辑的验证

     */

    @Override

    protected AuthenticationInfo doGetAuthenticationInfo(

            AuthenticationToken authenticationToken)

            throws AuthenticationException {

        UsernamePasswordCaptchaToken token = (UsernamePasswordCaptchaToken) authenticationToken;

        String username = token.getUsername();

        try {

            // 用户名密码非空验证

            if (StringUtils.isBlank(username)) {

                throw new AccountException(I18nResourceUtil.getResource(

                        "username.cannot.null", token.getLanguage())/* 用户名不能为空 */)

                        .initErrorCode(ERROR_420);

            }

            if (token.getPassword() == null || token.getPassword().length == 0) {

                throw new CredentialsException(I18nResourceUtil.getResource(

                        "password.cannot.null", token.getLanguage())/* 密码不能为空 */)

                        .initErrorCode(ERROR_421);

            }

            // 如果要求输入验证码,则验证是否正确

            Serializable sessionId = SecurityUtils.getSubject().getSession()

                    .getId();

            logger.info("验证sessionId: {} 的图片验证码: {}", sessionId,

                    token.getCaptcha());

            // 验证码判断逻辑,普通登录还会进行账户和IP登录失败次数限制验证,第三方登录和dmp登录不受该限制

//          captchaCheckProcessor(token, username, sessionId);

            User user = shiroUserService.findByUsername(username);

            if (user == null) {

                // 根据账号没有查询出用户则用户不存在,提示信息是:用户名或密码错误

                String text = I18nResourceUtil.getResource(

                        "username.or.password.error", token.getLanguage());

                shiroUserService.recordLoginLog(username, null, text);

                throw new UnknownAccountException(text)

                        .initErrorCode(ERROR_422);

            } else {

                // TODO 原本protected AuthenticationInfo

                // doGetAuthenticationInfo巨大无比,然后就将代码拆成了两个方法,在提取代码的时候就多了好几个return

                // 其实就是返回一个AuthenticationInfo的实例,下一步是要将这个return 去掉

                // 在本方法体中new出AuthenticationInfo,而不是多层代码嵌套然后在最深层产生该实例

                return doGetAuthenticationInfo(token, user);

            }

            

        } catch (AuthenticationException e) {

            if (!StringUtils.isBlank(username)) {

                shiroUserService.recordLoginFailureLog(username);// 保存一个错误登录日志

            }

            throw e;

        }

    }

    /**

     * 判断用户是否需要输入验证码,若需要,则进行验证码判断

     */

    private void captchaCheckProcessor(UsernamePasswordCaptchaToken token,

            String username, Serializable sessionId) {

        if (shiroUserService.captchaValidateByIp(ClientInfoHolder

                        .getClientInfo().getIp()) > 0 || shiroUserService

                        .captchaValidateByAccount(username) > 0) {

            if (token.getCaptcha() == null || token.getCaptcha().length() == 0) {

                shiroUserService.clearCaptchaLoginVerification(sessionId);

                throw new IncorrectCaptchaException(

                        I18nResourceUtil.getResource("captcha.cannot.null",

                                token.getLanguage())/* 验证码不能为空 */)

                        .initErrorCode(ERROR_423);

            } else {

                validateLoginFailureLimit(token, username, sessionId);

                validateCaptcha(token, sessionId);

                shiroUserService.clearCaptchaLoginVerification(sessionId);

            }

        }

    }

    /**

     * 验证一定周期内当前IP和账号登录失败次数是否在限定值范围内

     * @param token

     * @param username

     * @param sessionId

     */

    private void validateLoginFailureLimit(UsernamePasswordCaptchaToken token,

            String username, Serializable sessionId) {

        int failureCap = ReloadableConfig.HOLDER.getConfig()

                .getLoginFailureCap();

        int checkHours = ReloadableConfig.HOLDER.getConfig()

                .getLoginFailureCheckHours();

        if (shiroUserService.captchaValidateByIp(ClientInfoHolder.getClientInfo()

                .getIp()) > failureCap) {

            // 您的IP {}小时内登录错误已超过{}次, 该IP将被锁定{}小时

            throw new CredentialsException(I18nResourceUtil.getResource(

                    "over.gap.ip", token.getLanguage(), checkHours, failureCap,

                    checkHours)).initErrorCode(ERROR_424);

        } else if (shiroUserService.captchaValidateByAccount(username) > failureCap) {

            // 该账号{}小时内登录错误已超过{}次, 该账号将被锁定{}小时

            throw new CredentialsException(I18nResourceUtil.getResource(

                    "over.gap.account", token.getLanguage(), checkHours,

                    failureCap, checkHours)).initErrorCode(ERROR_425);

        }

    }

    /**

     * 检查输入的验证码是否正确

     * @param token

     * @param sessionId

     */

    private void validateCaptcha(UsernamePasswordCaptchaToken token,

            Serializable sessionId) {

        if (!shiroUserService.validateCaptchaLoginVerification(sessionId,

                token.getCaptcha())) {

            shiroUserService.clearCaptchaLoginVerification(sessionId);

            throw new IncorrectCaptchaException(I18nResourceUtil.getResource(

                    "captcha.error", token.getLanguage())/* 验证码错误 */)

                    .initErrorCode(ERROR_426);

        }

    }

    private AuthenticationInfo doGetAuthenticationInfo(

            UsernamePasswordCaptchaToken token, User user)

            throws AuthenticationException {

        String username = token.getUsername();

        checkActiveAndRemoveProcessor(token, user, username);// 验证用户状态

        boolean checkPasswordPass = false;

        if(token.isThirdLogin() && !StringUtils.isBlank(token.getThirdtoken())){

            checkPasswordPass = true;

        }else{

            //这个checkPassword就是我之前改造的那个方法。常量那个,在最上边

            checkPasswordPass = shiroUserService.checkPassword(user, new String(token.getPassword()));

        }

        

        /*

         * 判断账号是否已经登录邮箱确认

         */

        if (!checkPasswordPass) {

            shiroUserService.recordLoginLog(username, false);

            throw new IncorrectCredentialsException(

                    I18nResourceUtil.getResource("username.or.password.error",

                            token.getLanguage())/* 用户名或密码错误 */)

                    .initErrorCode(ERROR_427);

        } else {

            // 密码验证成功后进行的一些业务逻辑控制 包括 用户是否未审核通过

            // 用户同时在线白名单逻辑控制、用户三个月未修改密码,登录时强制用户修改代码 若都通过,则登录成功

            return checkPasswordSuccessProcessor(token, user, username);

        }

    }

    /**

     * 验证密码成功后进行的一些业务逻辑控制,包括:

     *  用户同时在线白名单逻辑控制

     *  用户三个月未修改密码

     * @param token

     * @param user

     * @param username

     * @return

     */

    protected AuthenticationInfo checkPasswordSuccessProcessor(

            UsernamePasswordCaptchaToken token, User user, String username) {

        shiroUserService.recordLoginLog(username, true);

        try {

            byte[] salt = Hex.decodeHex(user.getSalt().toCharArray());

            //shiroUserService.deleteLoginFailure(user);

            // 登录成功后,删除用户失败数据,重新开始

            // 同一账号同时在线数白名单逻辑控制

            //loginConcurrentLimitWhilteAccountProcessor(token, user);

            

            // 用户三个月未修改密码,登录时强制用户修改代码 

            // 取消不再验证密码三个月过期2017-11-28

            //passwordOutDateProcessor(token, user);

            //OEM定制 设置账号的themeName

//          user = userService.findOne(user.getId());

//          user.setChangeLastModifiedField(false);

//          user.setTheme(domainThemeMapper.getThemeNameByDomain(token.getDomain()));

//          userService.save(user);

            // 登录成功

//          return new SimpleAuthenticationInfo(new ShiroUser(user, partnerPage

//                  .getContent().get(0).getId(), token.getLanguage(),

//                  token.getDomain()), user.getPassword(),

//                  ByteSource.Util.bytes(salt), getName());

            return new SimpleAuthenticationInfo(new ShiroUser(user, token.getLanguage(),token.getDomain()), user.getPassword(),ByteSource.Util.bytes(salt), getName());

        } catch (DecoderException e) {

            logger.error(ExceptionUtils.getStackTrace(e));

            throw new RuntimeException(e);

        }

    }

    /**

     * 验证账号的同时在线人数是否符合要求

     */

    protected void loginConcurrentLimitWhilteAccountProcessor(

            UsernamePasswordCaptchaToken token, User user) {

        // 验证该账号同时在线数是否符合限制

        int loginConcurrentLimit = ReloadableConfig.HOLDER.getConfig()

                .getLoginConcurrentCap();

        List<SessionRecord> srs = shiroUserService.getLoginRecord(user.getId());

        if (srs.size() >= loginConcurrentLimit) {

            // 该账号同时登录了{}次,已达到最大限制

            throw new LoginConcurrentLimitException(

                    I18nResourceUtil.getResource(

                            "over.gap.account.concurrent",

                            token.getLanguage(), srs.size()))

                    .initErrorCode(ERROR_429);

        }

    }

    /**

     * 检查账号的激活和删除状态

     */

    private void checkActiveAndRemoveProcessor(

            UsernamePasswordCaptchaToken token, User user, String username) {

        if (!user.isActive()) {

            // 用户已被锁定

            String text = I18nResourceUtil.getResource("user.inactive",

                    token.getLanguage());

            shiroUserService.recordLoginLog(username, null, text);

            throw new LockedAccountException(text).initErrorCode(ERROR_433);

        } else if (user.isRemoved()) {

            // 用户已被屏蔽

            String text = I18nResourceUtil.getResource("user.removed",

                    token.getLanguage());

            shiroUserService.recordLoginLog(username, null, text);

            throw new DisabledAccountException(text).initErrorCode(ERROR_434);

        }

    }

    /**

     * 清空用户关联权限认证,待下次使用时重新加载。

     * 

     * @param account

     */

    public void clearCachedAuthorizationInfo(String account) {

        ShiroUser shiroUser = new ShiroUser();

        shiroUser.setAccount(account);

        shiroUser.setRealAccount(account);

        SimplePrincipalCollection principals = new SimplePrincipalCollection(

                shiroUser, getName());

        clearCachedAuthorizationInfo(principals);

    }

    /**

     * 清空所有关联认证

     */

    public void clearAllCachedAuthorizationInfo() {

        Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();

        if (cache != null) {

            cache.clear();

        }

    }

    

}

class AlwaysPassCredentialsMatcher implements CredentialsMatcher {

    /*

     * (non-Javadoc)

     * 

     * @see

     * org.apache.shiro.authc.credential.CredentialsMatcher#doCredentialsMatch

     * (org.apache.shiro.authc.AuthenticationToken,

     * org.apache.shiro.authc.AuthenticationInfo)

     */

    @Override

    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {

        return true;

    }

}
 

 

# sso-shiro-cas spring下使用shiro+cas配置单点登录,多个系统之间的访问,每次只需要登录一次 ## 系统模块说明 1. cas: 单点登录模块,这里直接拿的是cas的项目改了点样式而已 2. doc: 文档目录,里面有数据库生成语句,采用的是MySQL5.0,数据库名为db_test 3. spring-node-1: 应用1 4. spring-node-2: 应用2 其中node1跟node2都是采用spring + springMVC + mybatis 框架,使用maven做项目管理 ## cas集成说明 1.首先采用的是查数据库的方式来校验用户身份的,在cas/WEB-INF/deployerConfigContext.xml中第135行构建了这个类型 ``` xml ``` 其中QueryDatabaseAuthenticationHandler这个类是自定义构建的,在cas/WEB-INF/lib/cas-jdbc-1.0.0.jar里面,有兴趣的同学可以发编译看下,关于几个属性的说明 1. dataSource: 数据源,配置MySQL的连接信息 2. passwordEncoder: 加密方式,这里用的是MD5 3. sql: sql查询语句,这个语句就是根据用户输入的账号查询其密码 #### 以上就是单点登录管理的主要配置 ## 应用系统的配置node1 1. 应用系统采用shiro做权限控制,并且跟cas集成 2. 在/spring-node-1/src/main/resources/conf/shiro.properties 文件中 ``` properties shiro.loginUrl=http://127.0.0.1:8080/cas/login?service=http://127.0.0.1:8081/node1/shiro-cas shiro.logoutUrl=http://127.0.0.1:8080/cas/logout?service=http://127.0.0.1:8081/node1/shiro-cas shiro.cas.serverUrlPrefix=http://127.0.0.1:8080/cas shiro.cas.service=http://127.0.0.1:8081/node1/shiro-cas shiro.failureUrl=/users/loginSuccess shiro.successUrl=/users/loginSuccess ``` 其中shiro.loginUrl 跟 shiro.logoutUrl的前面是cas验证的地址,后面的是我们应用系统的地址,这样配置的方式是为了在访问我们的应用系统的时候,先到cas进行验证,如果验证成功了,cas将重定向到shiro.successUrl 所表示的地址 3.在/spring-node-1/src/main/resources/conf/shiro.xml 文件中 ``` xml /shiro-cas = casFilter /logout = logoutFilter /users/** = user ``` > 其中shiroFilter这个类主要用于需要拦截的url请求,需要注意的是这个是shiro的拦截,我们还需要配置cas的过滤配置casFilter > casRealm这个类是需要我们自己实现的,主要用于shiro的权限验证,里面的属性说明如下 1. defaultRoles: 默认的角色 2. casServerUrlPrefix: cas地址 3. casService: 系统应用地址 最后我们还需要在/spring-node-1/src/main/webapp/WEB-INF/web.xml 文件中配置相关的过滤器拦截全部请求 ``` xml shiroFilter org.springframework.web.filter.DelegatingFilterProxy targetFilterLifecycle true shiroFilter /* ``` ## 系统运行 1. 端口说明,cas:8080,node1:8081,node2:8082,大家可以采用maven提供的tomcat7插件,配置如下: ``` xml org.apache.tomcat.maven tomcat7-maven-plugin 2.1 8081 UTF-8 tomcat7 /node1 ``` 这样的配置,我们甚至都不需要配置tomcat服务器了,建议这种方式 2.各个模块的访问地址 > cas:http://127.0.0.1:8080/cas > node1:http://127.0.0.1:8081/node1 > node2:http://127.0.0.1:8082/node2 3.访问系统 > 输入 http://127.0.0.1:8081/node1/shiro-cas ,进入cas验证 > 输入用户名 admin,密码 admin@2015,验证成功后将会重定向到http://127.0.0.1:8081/node1//users/loginSuccess ,也就是node1系统的主页,里面的节点2代表的是node2系统的主页,你会发现我们不需要登录到node2系统就能访问其中的系统
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值