更新時間:2022-06-21 11:42:24 來源:動力節點 瀏覽1567次
簡單來講,就是在一個系統登陸過后,進入其他系統不需要再次登陸,具體舉個例子來講,在訪問業務B系統時,由于沒有登陸過,先跳到單點登陸A系統進行登陸,在A系統登陸完成之后,跳回到業務B系統的首頁,與此同時,直接訪問業務C系統不需要進行登陸
用戶訪問頁面會在服務端都會產生一個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里面的形式。如果想了解更多相關知識,可以關注一下SSO單點登錄實現的工作原理。
先說下單點登陸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是否有效,如果無效,跳轉到單點登陸頁面進行登陸以及信息綁定,如果有效,進行正常操作。如果大家想了解更多相關知識,可以關注一下動力節點的Shiro視頻教程,里面有更豐富的知識等著大家去學習,希望對大家能夠有所幫助。
0基礎 0學費 15天面授
有基礎 直達就業
業余時間 高薪轉行
工作1~3年,加薪神器
工作3~5年,晉升架構
提交申請后,顧問老師會電話與您溝通安排學習