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

2018-03-16  本文已影响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代码,方便大家一起参考学习
直接上地址

上一篇下一篇

猜你喜欢

热点阅读