更新時(shí)間:2022-06-21 11:42:24 來源:動力節(jié)點(diǎn) 瀏覽2093次
簡單來講,就是在一個系統(tǒng)登陸過后,進(jìn)入其他系統(tǒng)不需要再次登陸,具體舉個例子來講,在訪問業(yè)務(wù)B系統(tǒng)時(shí),由于沒有登陸過,先跳到單點(diǎn)登陸A系統(tǒng)進(jìn)行登陸,在A系統(tǒng)登陸完成之后,跳回到業(yè)務(wù)B系統(tǒng)的首頁,與此同時(shí),直接訪問業(yè)務(wù)C系統(tǒng)不需要進(jìn)行登陸
用戶訪問頁面會在服務(wù)端都會產(chǎn)生一個Session,同時(shí)在瀏覽器也需要把這個Session對應(yīng)的SessionID保存下來,如果登陸過后就會給這個Session綁定上用戶信息。
Session的能在任何系統(tǒng)產(chǎn)生,但是進(jìn)行用戶信息的綁定需要在單點(diǎn)登陸A系統(tǒng)進(jìn)行。在訪問單點(diǎn)登陸A系統(tǒng)或者業(yè)務(wù)B,C系統(tǒng)時(shí),都會從瀏覽器把SessionID帶到服務(wù)器,服務(wù)器在攔截器通過SessionID獲取Session,如果獲取不到Session或者Session無效就會重定向到單點(diǎn)登陸A系統(tǒng)的登陸頁面。
瀏覽器保存SessionID的方式放在Cookie里面,優(yōu)點(diǎn)是客戶端對此無感知,缺點(diǎn)是Cookie和域名存在綁定關(guān)系,必須放在一級域名下面放在LocalStorage,請求的時(shí)候放在url后面或者h(yuǎn)eader里面都可在shiro中主要使用cookie存放sessionid,不過也兼容放在url里面的形式。如果想了解更多相關(guān)知識,可以關(guān)注一下SSO單點(diǎn)登錄實(shí)現(xiàn)的工作原理。
先說下單點(diǎn)登陸A系統(tǒng)的實(shí)現(xiàn),該系統(tǒng)主要提供一個登陸頁面,登陸成功后會給當(dāng)前Session綁定用戶信息,Session存儲在redis中,這樣其他子系統(tǒng)也能通過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>
該頁面會把登陸前的頁面保存下來,一旦調(diào)用登陸接口成功,通過window.location.href=redirectUrl進(jìn)行回跳
看下登陸接口的實(shí)現(xiàn)
@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進(jìn)行登陸驗(yàn)證,成功后會把用戶信息綁定到Session,login方法底層會通過我們配置的AuthenticatingRealm實(shí)現(xiàn)進(jìn)行登陸驗(yàn)證
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;
}
}
同時(shí)Session保存在Redis中,我們通過繼承AbstractSessionDAO實(shí)現(xiàn)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;
}
}
在來講下被單點(diǎn)登陸控制的子系統(tǒng),它們都需要引入ShiroFilter對需要進(jìn)行登陸驗(yàn)證的請求進(jìn)行攔截,這邊對ShiroFilter對配置進(jìn)行了抽象,由于是用了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;
}
}
留了擴(kuò)展方法buildFilterChainDefinitionMap給子類用于實(shí)現(xiàn)自定義的攔截,例如
@Configuration
public class ShiroConfig extends AbstractShiroConfig{
@Override
public Map<String, String> buildFilterChainDefinitionMap() {
Map<String, String> config = new HashMap<>();
config.put("/**","authc");
return config;
}
}
這就是對該系統(tǒng)所有請求都需要進(jìn)行登陸驗(yàn)證
這個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之后,對于需要驗(yàn)證的請求,都會通過sessionid去取Session,判斷Session是否有效,如果無效,跳轉(zhuǎn)到單點(diǎn)登陸頁面進(jìn)行登陸以及信息綁定,如果有效,進(jìn)行正常操作。如果大家想了解更多相關(guān)知識,可以關(guān)注一下動力節(jié)點(diǎn)的Shiro視頻教程,里面有更豐富的知識等著大家去學(xué)習(xí),希望對大家能夠有所幫助。

初級 202925

初級 203221

初級 202629

初級 203743