1.什么是单点登陆
简单来讲,就是在一个系统登陆过后,进入其他系统不需要再次登陆
具体举个例子来讲,在访问业务B系统时,由于没有登陆过,先跳到单点登陆A系统进行登陆,在A系统登陆完成之后,跳回到业务B系统的首页,与此同时,直接访问业务C系统不需要进行登陆
2.单点登陆实现的原理
用户访问页面会在服务端都会产生一个Session,同时在浏览器也需要把这个Session对应的SessionID保存下来,如果登陆过后就会给这个Session绑定上用户信息。
Session的能在任何系统产生,但是进行用户信息的绑定需要在单点登陆A系统进行。
在访问单点登陆A系统或者业务B,C系统时,都会从浏览器把SessionID带到服务器,服务器在拦截器通过SessionID获取Session,如果获取不到Session或者Session无效就会重定向到单点登陆A系统的登陆页面。
浏览器保存SessionID的方式
- 放在Cookie里面,优点是客户端对此无感知,缺点是Cookie和域名存在绑定关系,必须放在一级域名下面
- 放在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代码,方便大家一起参考学习
直接上地址
网友评论