美文网首页
基于Shiro实现的单点登陆系统

基于Shiro实现的单点登陆系统

作者: 土豆肉丝盖浇饭 | 来源:发表于2018-03-16 00:42 被阅读0次

    1.什么是单点登陆

    简单来讲,就是在一个系统登陆过后,进入其他系统不需要再次登陆
    具体举个例子来讲,在访问业务B系统时,由于没有登陆过,先跳到单点登陆A系统进行登陆,在A系统登陆完成之后,跳回到业务B系统的首页,与此同时,直接访问业务C系统不需要进行登陆

    2.单点登陆实现的原理

    用户访问页面会在服务端都会产生一个Session,同时在浏览器也需要把这个Session对应的SessionID保存下来,如果登陆过后就会给这个Session绑定上用户信息。
    Session的能在任何系统产生,但是进行用户信息的绑定需要在单点登陆A系统进行。
    在访问单点登陆A系统或者业务B,C系统时,都会从浏览器把SessionID带到服务器,服务器在拦截器通过SessionID获取Session,如果获取不到Session或者Session无效就会重定向到单点登陆A系统的登陆页面。

    浏览器保存SessionID的方式

    1. 放在Cookie里面,优点是客户端对此无感知,缺点是Cookie和域名存在绑定关系,必须放在一级域名下面
    2. 放在LocalStorage,请求的时候放在url后面或者header里面都可

    在shiro中主要使用cookie存放sessionid,不过也兼容放在url里面的形式

    3.结合shiro实现单点登陆系统

    先说下单点登陆A系统的实现,该系统主要提供一个登陆页面,登陆成功后会给当前Session绑定用户信息,Session存储在redis中,这样其他子系统也能通过SessionID获取到
    先看下登陆页面的代码

    <html xmlns:th="http://www.thymeleaf.org">
    <head>
        <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    </head>
    <body>
        <p>hello world</p>
        <form>
            <input type="text" id="username" name="username">
            <input type="password" id="password" name="password"/>
            <input type="hidden" id="redirectUrl" th:value="${redirectUrl}"/>
            <input type="submit" id="loginButton" value="登录"/>
        </form>
        <script>
    
            $(function () {
                $('#loginButton').click(function (event) {
                    event.preventDefault()
                    var username = $('#username').val();
                    var password = $('#password').val();
                    var redirectUrl = $('#redirectUrl').val();
                    $.post("/login",{
                        username:username,
                        password:password
                    },function (result) {
                        console.log(JSON.stringify(result));
                        if(result.flag==true){
                            window.location.href=redirectUrl;
                        }
                    },"json")
                })
            })
        </script>
    </body>
    </html>
    

    该页面会把登陆前的页面保存下来,一旦调用登陆接口成功,通过window.location.href=redirectUrl进行回跳
    看下登陆接口的实现

    @PostMapping("/login")
        @ResponseBody
        public WebResult login(@RequestParam("username")String username,@RequestParam("password")String password){
    
            UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);
    
            try {
                Subject subject = SecurityUtils.getSubject();
                subject.login(usernamePasswordToken);
            }catch(Exception ex){
                logger.error("登录失败",ex);
                return new WebResult(null,false);
            }
    
            return new WebResult(null,true);
        }
    

    通过subject.login进行登陆验证,成功后会把用户信息绑定到Session,login方法底层会通过我们配置的AuthenticatingRealm实现进行登陆验证

    public class AuthenticationRealm extends AuthenticatingRealm{
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            UsernamePasswordToken usernamePasswordToken =(UsernamePasswordToken)authenticationToken;
            if("scj".equals(usernamePasswordToken.getUsername())&&"123456".equals(new String(usernamePasswordToken.getPassword()))){
                Principal principal = new Principal();
                principal.setUserId(1L);
                principal.setUsername("盛超杰");
                principal.setTelephone("13388611621");
                return new SimpleAuthenticationInfo(principal,((UsernamePasswordToken) authenticationToken).getPassword(),getName());
            }
    
            throw new IncorrectCredentialsException("账户名或密码错误");
        }
    
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof UsernamePasswordToken;
        }
    }
    

    同时Session保存在Redis中,我们通过继承AbstractSessionDAO实现RedisSessionDAO来完成这个功能

    public class RedisSessionDAO extends AbstractSessionDAO{
    
        private static final String REDIS_SESSION_KEY ="SSO:REDIS_SESSION_KEY";
    
        private StringRedisTemplate stringRedisTemplate;
    
        private Serialization serialization;
    
        @Override
        protected Serializable doCreate(Session session) {
            Serializable sessionId = this.generateSessionId(session);
            this.assignSessionId(session, sessionId);
            stringRedisTemplate.execute(new RedisCallback<Object>() {
                @Nullable
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    connection.hSet(REDIS_SESSION_KEY.getBytes(),sessionId.toString().getBytes(),serialization.seralize(session));
                    return null;
                }
            });
            return sessionId;
        }
    
        @Override
        protected Session doReadSession(Serializable serializable) {
            return (Session) stringRedisTemplate.execute(new RedisCallback<Object>() {
                @Nullable
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    byte[] bytes = connection.hGet(REDIS_SESSION_KEY.getBytes(),serializable.toString().getBytes());
                    return serialization.deseralize(bytes);
                }
            });
        }
    
        @Override
        public void update(Session session) throws UnknownSessionException {
            stringRedisTemplate.execute(new RedisCallback<Object>() {
                @Nullable
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    connection.hSet(REDIS_SESSION_KEY.getBytes(),session.getId().toString().getBytes(),serialization.seralize(session));
                    return null;
                }
            });
        }
    
        @Override
        public void delete(Session session) {
            stringRedisTemplate.opsForHash().delete(REDIS_SESSION_KEY,session.getId().toString());
        }
    
        @Override
        public Collection<Session> getActiveSessions() {
            List<Session> sessionList = new ArrayList<>();
            Set<Object> keys = stringRedisTemplate.opsForHash().keys(REDIS_SESSION_KEY);
            for (Object key:keys){
                sessionList.add((Session) stringRedisTemplate.execute(new RedisCallback<Object>() {
                    @Nullable
                    @Override
                    public Object doInRedis(RedisConnection connection) throws DataAccessException {
                        byte[] bytes = connection.hGet(REDIS_SESSION_KEY.getBytes(),key.toString().getBytes());
                        return serialization.deseralize(bytes);
                    }
                }));
            }
            return sessionList;
        }
    
        public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
            this.stringRedisTemplate = stringRedisTemplate;
        }
    
        public void setSerialization(Serialization serialization) {
            this.serialization = serialization;
        }
    }
    

    在来讲下被单点登陆控制的子系统,它们都需要引入ShiroFilter对需要进行登陆验证的请求进行拦截
    我这边对ShiroFilter对配置进行了抽象,由于是用了Springboot,所以配置也没用xml,使用java类的配置

    @Configuration
    public abstract class AbstractShiroConfig {
    
       @Value("${sso.successUrl}")
       private String successUrl;
    
       @Value("${sso.loginUrl}")
       private String loginUrl;
    
       @Value("${sso.cookie.domain}")
       private String cookieDomain;
    
       @Bean
       public FilterRegistrationBean filterRegistrationBean(){
           FilterRegistrationBean filterRegistrationBean =new FilterRegistrationBean();
           filterRegistrationBean.setFilter(new DelegatingFilterProxy());
           filterRegistrationBean.setName("shiroFilter");
           filterRegistrationBean.addUrlPatterns("/*");
           filterRegistrationBean.addInitParameter("targetFilterLifecycle","true");
           return filterRegistrationBean;
       }
    
       @Bean
       public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
           ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
           shiroFilterFactoryBean.setSecurityManager(securityManager);
           shiroFilterFactoryBean.setSuccessUrl(successUrl);
           shiroFilterFactoryBean.setLoginUrl(loginUrl);
           shiroFilterFactoryBean.setFilterChainDefinitionMap(buildFilterChainDefinitionMap());
           return shiroFilterFactoryBean;
       }
    
       public abstract Map<String, String> buildFilterChainDefinitionMap();
    
       @Bean
       public SecurityManager securityManager(SessionManager sessionManager){
           DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
           securityManager.setSessionManager(sessionManager);
           securityManager.setRealm(new AuthenticationRealm());
           return securityManager;
       }
    
       @Bean
       public SessionManager sessionManager(SimpleCookie simpleCookie,SessionDAO sessionDAO){
           DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
           sessionManager.setSessionIdCookie(simpleCookie);
           sessionManager.setSessionIdCookieEnabled(true);
           sessionManager.setSessionDAO(sessionDAO);
           sessionManager.setGlobalSessionTimeout(1800000L);
           return sessionManager;
       }
    
       @Bean
       public SessionDAO sessionDAO(StringRedisTemplate stringRedisTemplate){
           RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
           redisSessionDAO.setStringRedisTemplate(stringRedisTemplate);
           redisSessionDAO.setSerialization(new JDKSerialization());
           return redisSessionDAO;
       }
    
       @Bean
       public SimpleCookie simpleCookie(){
           SimpleCookie simpleCookie = new SimpleCookie();
           simpleCookie.setPath("/");
           simpleCookie.setDomain(cookieDomain);
           simpleCookie.setName("SCJSESSIONID");
           simpleCookie.setMaxAge(SimpleCookie.ONE_YEAR);
           return simpleCookie;
       }
    
    }
    

    留了扩展方法buildFilterChainDefinitionMap给子类用于实现自定义的拦截,例如

    @Configuration
    public class ShiroConfig extends AbstractShiroConfig{
        @Override
        public Map<String, String> buildFilterChainDefinitionMap() {
            Map<String, String> config = new HashMap<>();
            config.put("/**","authc");
            return config;
        }
    }
    

    这就是对该系统所有请求都需要进行登陆验证

    这个Filter如何整合到Servlet容器里面去,看上面代码的第一个bean

    @Bean
        public FilterRegistrationBean filterRegistrationBean(){
            FilterRegistrationBean filterRegistrationBean =new FilterRegistrationBean();
            filterRegistrationBean.setFilter(new DelegatingFilterProxy());
            filterRegistrationBean.setName("shiroFilter");
            filterRegistrationBean.addUrlPatterns("/*");
            filterRegistrationBean.addInitParameter("targetFilterLifecycle","true");
            return filterRegistrationBean;
        }
    

    这是Spring提供的免配置化的注册方式
    在配置了ShiroFilter之后,对于需要验证的请求,都会通过sessionid去取Session,判断Session是否有效,如果无效,跳转到单点登陆页面进行登陆以及信息绑定,如果有效,进行正常操作

    4.代码分享

    上面的这些当然是我已经写好的Demo代码,方便大家一起参考学习
    直接上地址

    相关文章

      网友评论

          本文标题:基于Shiro实现的单点登陆系统

          本文链接:https://www.haomeiwen.com/subject/lgvkqftx.html