系統簡介
Http協議
web應用采用browser/server架構,http作為通信協議,http是無狀態協議,瀏覽器的每一次請求,服務器會獨立處理,不與之前或之后的請求產生關聯,這個程序用下圖說明,三次請求/回應對之間沒有任何聯系,

但這也同時意味著,任何用戶都能通過瀏覽器訪問服務器資源,如果想保護服務器的某些資源,必須限制瀏覽器請求;要限制瀏覽器請求,必須鑒別瀏覽器請求,回應合法請求,忽略非法請求;要鑒別瀏覽器請求,必須清楚瀏覽器請求狀態,既然http協議無狀態,那就讓服務器和瀏覽器共同維護一個狀態吧!這就是會話機制,
有狀態會話
瀏覽器第一次請求服務器,服務器創建一個會話,并將會話的id作為回應的一部分發送給瀏覽器,瀏覽器存盤會話id,并在后續第二次和第三次請求中帶上會話id,服務器取得請求中的會話id就知道是不是同一個用戶了,這個程序用下圖說明,后續請求與第一次請求產生了關聯,

服務器在記憶體中保存會話物件,瀏覽器怎么保存會話id呢?那就瀏覽器自己來維護這個會話id,每次發送http請求時瀏覽器自動發送會話id,cookie機制正好用來做這件事,cookie是瀏覽器用來存盤少量資料的一種機制,資料以”key/value“形式存盤,瀏覽器發送http請求時自動附帶cookie資訊
tomcat會話機制當然也實作了cookie,訪問tomcat服務器時,瀏覽器中可以看到一個名為“JSESSIONID”的cookie,這就是tomcat會話機制維護的會話id,使用了cookie的請求回應程序如下圖

記錄登陸狀態
有了會話機制,登錄狀態就好明白了,我們假設瀏覽器第一次請求服務器需要輸入用戶名與密碼驗證身份,服務器拿到用戶名密碼去資料庫比對,正確的話說明當前持有這個會話的用戶是合法用戶,應該將這個會話狀態進行保存,例如:

單點登錄系統設計
概述
web系統早已從久遠的單系統發展成為如今由多系統組成的應用群,面對如此眾多的系統,用戶難道要一個一個登錄、然后一個一個注銷嗎?就像下圖描述的這樣,例如

Web系統由單系統發展成多系統組成的應用群,復雜性應該由系統內部承擔,而不是用戶,無論web系統內部多么復雜,對用戶而言,都是一個統一的整體,也就是說,用戶訪問web系統的整個應用群與訪問單個系統一樣,登錄/注銷只要一次就夠了

雖然單系統的登錄解決方案很完美,但對于多系統應用群已經不再適用了,為什么呢?
單系統登錄解決方案的核心是cookie,cookie攜帶會話id在瀏覽器與服務器之間維護會話狀態,但cookie是有限制的,這個限制就是cookie的域(通常對應網站的域名),瀏覽器發送http請求時會自動攜帶與該域匹配的cookie,而不是所有cookie,例如:

既然這樣,為什么不將web應用群中所有子系統的域名統一在一個頂級域名下,例如“*.baidu.com”,然后將它們的cookie域設定為“baidu.com”,這種做法理論上是可以的,甚至早期很多多系統登錄就采用這種同域名共享cookie的方式,
然而,可行并不代表好,共享cookie的方式存在眾多局限,首先,應用群域名得統一;其次,應用群各系統使用的技術(至少是web服務器)要相同,不然cookie的key值(tomcat為JSESSIONID)不同,無法維持會話,共享cookie的方式是無法實作跨語言技術平臺登錄的,比如java、php、.net系統之間;第三,cookie本身不安全,
因此,我們需要一種全新的登錄方式來實作多系統應用群的登錄,這就是單點登錄, 什么是單點登錄?單點登錄全稱Single Sign On(以下簡稱SSO),是指在多系統應用群中登錄一個系統,便可在其他所有系統中得到授權而無需再次登錄,包括單點登錄與單點注銷兩部分,
登陸業務設計
相比于單系統登錄,sso需要一個獨立的認證中心,只有認證中心能接受用戶的用戶名密碼等安全資訊,其他系統不提供登錄入口,只接受認證中心的間接授權,間接授權通過令牌實作,sso認證中心驗證用戶的用戶名密碼沒問題,創建授權令牌,在接下來的跳轉程序中,授權令牌作為引數發送給各個子系統,子系統拿到令牌,即得到了授權,可以借此創建區域會話,區域會話登錄方式與單系統的登錄方式相同,這個程序,也就是單點登錄的原理,用下圖說明:

下面對上圖簡要描述
1)用戶訪問系統1的受保護資源,系統1發現用戶未登錄,跳轉至sso認證中心,并將自己的地址作為引數
2) sso認證中心發現用戶未登錄,將用戶引導至登錄頁面
3) 用戶輸入用戶名密碼提交登錄申請
4) sso認證中心校驗用戶資訊,創建用戶與sso認證中心之間的會話,稱為全域會話,同時創建授權令牌
5) sso認證中心帶著令牌跳轉回最初的請求地址(系統1)
6) 系統1拿到令牌,去sso認證中心校驗令牌是否有效
7) sso認證中心校驗令牌,回傳有效,注冊系統1
8) 系統1使用該令牌創建與用戶的會話,稱為區域會話,回傳受保護資源
9) 用戶訪問系統2的受保護資源
10) 系統2發現用戶未登錄,跳轉至sso認證中心,并將自己的地址作為引數
11) sso認證中心發現用戶已登錄,跳轉回系統2的地址,并附上令牌
12) 系統2拿到令牌,去sso認證中心校驗令牌是否有效
13) sso認證中心校驗令牌,回傳有效,注冊系統2
14) 系統2使用該令牌創建與用戶的區域會話,回傳受保護資源
用戶登錄成功之后,會與sso認證中心及各個子系統建立會話,用戶與sso認證中心建立的會話稱為全域會話,用戶與各個子系統建立的會話稱為區域會話,區域會話建立之后,用戶訪問子系統受保護資源將不再通過sso認證中心,例如:

創建專案聚合工程
創建聚合工程的目的是對專案中的資源(例如一些依賴)進行統一管理,多個專案module之間共享資源.
這次專案的maven工程結構如下:
|—04-jt-sso
|–sso-auth #認證服務器
|–sso-resource #資源服務器
|–pom.xml #公共依賴及版本管理
創建父工程
第一步:創建父工程,名字為04-jt-sso,例如:

第二步:洗掉工程中的src目錄(parent工程一般不需要寫java代碼).
第三步:在pom.xml檔案中添加parent元素并指定springboot依賴.
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.3.2.RELEASE</version>
</parent>
創建認證工程
第一步:在04-jt-sso工程下創建sso-auth專案module,例如

第二步:打開pom.xml檔案,添加專案依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
第三步:創建application.yml組態檔,定義服務埠,例如
server:
port: 8081
第四步:撰寫專案啟動類.
package sso;
@SpringBootApplication
public class AuthApp {
public static void main(String[] args) {
SpringApplication.run(AuthApp.class,args);
}
}
第五步:啟動服務檢測是否可以啟動ok.
創建資源工程
第一步:在04-jt-sso工程下創建sso-resource專案module,例如

第二步:打開pom.xml檔案,添加專案依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
第三步:創建application.yml組態檔,定義服務埠,例如
server:
port: 8091
第四步:撰寫專案啟動類.
package sso;
@SpringBootApplication
public class ResApp {
public static void main(String[] args) {
SpringApplication.run(ResApp.class,args);
}
}
認證服務實作
工具類
第一步:定義JWT工具類,用于創建,決議,驗證token,代碼如下:
package sso.auth.util;
public class JwtUtils {
private static String secret="AAABBBCCCDDDEEE";
/**基于負載和演算法創建token資訊*/
public static String generatorToken(Map<String,Object> map){
return Jwts.builder()
.setClaims(map)
.setExpiration(new Date(System.currentTimeMillis()+30*60*1000))
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,secret)
.compact();//簽約,創建token
}
/**決議token獲取資料*/
public static Claims getClaimsFromToken(String token){
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
/**判定token是否失效*/
public static boolean isTokenExpired(String token){
Date expiration=getClaimsFromToken(token).getExpiration();
return expiration.before(new Date());
}
}
第二步:定義Web工具類,用于向客戶端回應json資料
package sso.auth.util;
public class WebUtils {
public static void writeJsonToClient(HttpServletResponse response, Map<String,Object> map)
throws IOException {
//1設定回應資料的編碼
response.setCharacterEncoding("utf-8");
//2告訴瀏覽器回應資料的內容型別以及編碼
response.setContentType("application/json;charset=utf-8");
//3獲取輸出流物件
PrintWriter out=response.getWriter();
//4 將map轉換為json資料
String result=new ObjectMapper().writeValueAsString(map);
//5 將資料回應到客戶端
out.println(result);
out.flush();
}
}
安全配置類
定義認證規則及例外處理,例如:
package sso.auth.config;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//1.關閉跨域攻擊
http.csrf().disable();
//2.配置登錄url(登錄表單使用哪個頁面)
http.formLogin()
.successHandler(authenticationSuccessHandler())
.failureHandler(authenticationFailureHandler());
//設定需要認證與拒絕訪問的例外處理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint());
//3.放行登錄url(不需要認證就可以訪問)
http.authorizeRequests()
.anyRequest().authenticated();//除了以上資源必須認證才可訪問
}
//認證成功處理器
public AuthenticationSuccessHandler authenticationSuccessHandler(){
return (httpServletRequest, httpServletResponse,authentication)-> {
User principal = (User)authentication.getPrincipal();
Map<String,Object> map=new HashMap<>();
map.put("state",200);
map.put("message","Login ok");
Map<String,Object> jwtMap=new HashMap<>();
jwtMap.put("username", principal.getUsername());
List<String> authorities = new ArrayList<>();
principal.getAuthorities().forEach((authority)-> {
authorities.add(authority.getAuthority());
});
jwtMap.put("authorities",authorities);
String token=JwtUtils.generatorToken(jwtMap);
map.put("token", token);
WebUtils.writeJsonToClient(httpServletResponse,map);
};
}
//認證失敗處理器
public AuthenticationFailureHandler authenticationFailureHandler(){
return (httpServletRequest, httpServletResponse, e) -> {
Map<String,Object> map=new HashMap<>();
map.put("state",500);
map.put("msg","username or password error");
WebUtils.writeJsonToClient(httpServletResponse,map);
};
}
//沒有認證時執行DefaultAuthenticationEntryPoint物件
public AuthenticationEntryPoint authenticationEntryPoint(){
return (httpServletRequest, httpServletResponse, e)->{
Map<String,Object> map=new HashMap<>();
map.put("state",401);//SC_UNAUTHORIZED 的值為401
map.put("message","請先登錄再訪問");
WebUtils.writeJsonToClient(httpServletResponse,map);
};
}
}
認證邏輯物件
定義業務物件,處理客戶端的登陸請求,例如:
package sso.auth.service;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.基于用戶名從資料庫查詢用戶資訊
//SysUser user=userMapper.selectUserByUsername(username);//查資料庫
if(!"jack".equals(username))//假設這是從資料庫查詢的資訊
throw new UsernameNotFoundException("user not exists");
//2.將用戶資訊封裝到UserDetails物件中并回傳
//假設這個密碼是從資料庫查詢出來的
String encodedPwd=passwordEncoder.encode("123456");
//假設這個權限資訊也是從資料庫查詢到的
//List<String> permissions=userMapper.selectUserPermissions(username);//查資料庫
//假如分配權限的方式是角色,撰寫字串時用"ROLE_"做前綴
List<GrantedAuthority> grantedAuthorities =
AuthorityUtils.commaSeparatedStringToAuthorityList( "sys:res:retrieve,sys:res:create");
//這個user是SpringSecurity提供的UserDetails介面的實作,用于封裝用戶資訊
//后續我們也可以基于需要自己構建UserDetails介面的實作
User user=new User(username,encodedPwd,grantedAuthorities);
return user;//這里的回傳值會交給springsecurity去校驗
}
}
此物件撰寫好以后,可以啟動服務基于postman進行登陸訪問測驗,
資源服務實作
工具類
第一步:定義JWT工具類,主要用于決議JWT令牌,例如
package sso.resource.util;
public class JwtUtils {
private static String secret="AAABBBCCCDDDEEE";
/**基于負載和演算法創建token資訊*/
public static String generatorToken(Map<String,Object> map){
return Jwts.builder()
.setClaims(map)
.setExpiration(new Date(System.currentTimeMillis()+30*60*1000))
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,secret)
.compact();//簽約,創建token
}
/**決議token獲取資料*/
public static Claims getClaimsFromToken(String token){
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
/**判定token是否失效*/
public static boolean isTokenExpired(String token){
Date expiration=getClaimsFromToken(token).getExpiration();
return expiration.before(new Date());
}
}
第二步:定義Web工具類,用于向客戶端回應json資料
package sso.auth.util;
public class WebUtils {
public static void writeJsonToClient(HttpServletResponse response, Map<String,Object> map)
throws IOException {
//1設定回應資料的編碼
response.setCharacterEncoding("utf-8");
//2告訴瀏覽器回應資料的內容型別以及編碼
response.setContentType("application/json;charset=utf-8");
//3獲取輸出流物件
PrintWriter out=response.getWriter();
//4 將map轉換為json資料
String result=new ObjectMapper().writeValueAsString(map);
//5 將資料回應到客戶端
out.println(result);
out.flush();
}
}
安全配置類
在資源服務中定義權限配置類,默認將所有認證請求放行,例如:
package sso.resource.config;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler());
http.authorizeRequests().anyRequest().permitAll();
}
//沒有權限時執行此處理器方法
public AccessDeniedHandler accessDeniedHandler(){
return (httpServletRequest, httpServletResponse, e)-> {
Map<String,Object> map=new HashMap<>();
map.put("state",403);//SC_FORBIDDEN的值是403
map.put("message","沒有訪問權限,請聯系管理員");
WebUtils.writeJsonToClient(httpServletResponse,map);
};
}
}
資源服務物件
定義資源服務物件,用于處理客戶端的資源訪問服務,例如:
package sso.resource.controller;
@RestController
public class ResourceController {
@PreAuthorize("hasAuthority('sys:res:create')")
@RequestMapping("/doCreate")
public String doCreate(HttpServletResponse response){
return "create resource (insert data) ok";
}
/**查詢操作*/
@PreAuthorize("hasAuthority('sys:res:retrieve')")
@RequestMapping("/doRetrieve")
public String doRetrieve(){//Retrieve 表示查詢
return "query resource (select data) ok";
}
/**修改操作*/
@PreAuthorize("hasAuthority('sys:res:update')")
@RequestMapping("/doUpdate")
public String doUpdate(){
return "update resource (update data) ok";
}
/**洗掉操作*/
@PreAuthorize("hasAuthority('sys:res:delete')")
@RequestMapping("/doDelete")
public String doDelete(){
return "delete resource (dalete data) ok";
}
}
Spring MVC攔截器
資源服務器中的資源不是所有人都可以訪問的,需要具備一定權限才可以,首先我們要判定是否登陸,然后判定登陸用戶是否有權限,有訪問權限才可以授權訪問,這個操作可以放到spring mvc攔截器中進行實作,例如:
第一步:定義Spring MVC 攔截器.
package sso.resource.interceptor;
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token=request.getHeader("token");
if(token==null||"".equals(token)) throw new RuntimeException("請先登陸");
if(JwtUtils.isTokenExpired(token)) throw new RuntimeException("請先登陸");
Claims claims=JwtUtils.getClaimsFromToken(token);
List<String> list = (List<String>) claims.get("authorities");
String[]authorities=list.toArray(new String[]{});
UserDetails userDetails= User.builder()
.username((String)claims.get("username"))
.password("")
.authorities(authorities)
.build();
//將UserDetails物件保存到一個可以和Spring-Security互動的物件中
PreAuthenticatedAuthenticationToken authenticationToken=
new PreAuthenticatedAuthenticationToken(
userDetails,userDetails.getPassword(),
AuthorityUtils.createAuthorityList(authorities));
//將本次決議的用戶詳情和當前請求關聯
//關聯之后才能在后面的控制器中獲得用戶詳情
authenticationToken.setDetails(new WebAuthenticationDetails(request));
//將當前用戶詳情保存到Spring-Security背景關系
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
return true;
}
}
第二步:創建Spring Web配置類,用于注冊和配置Spring MVC攔截器,例如:
package sso.resource.config;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenInterceptor())
.addPathPatterns("/**");
}
}
資源訪問測驗
第一步:啟動認證服務器,通過postman進行登陸認證,例如:

第二步:啟動資源服務器,并基于認證服務器回傳的令牌進行資源訪問

總結(summary)
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/289909.html
標籤:其他
下一篇:手機抓包(以抖音為例)
