背景分析
傳統的登錄系統中,每個站點都實現了自己的專用登錄模塊。各站點的登錄狀態相互不認可,各站點需要逐一手工登錄。例如:

這樣的系統,我們又稱之為多點登陸系統。應用起來相對繁瑣(每次訪問資源服務都需要重新登陸認證和授權)。與此同時,系統代碼的重複也比較高。由此單點登陸系統誕生。
單點登陸系統
單點登錄,英文是 Single Sign On(縮寫為 SSO)。即多個站點共用一台認證授權伺服器,用戶在其中任何一個站點登錄後,可以免登錄訪問其他所有站點。而且,各站點間可以通過該登錄狀態直接交互。例如:

快速入門實踐
工程結構如下
基於資源服務工程添加單點登陸認證和授權服務,工程結構定義如下:

創建認證授權工程

添加項目依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
構建項目配置文件
在sca-auth工程中創建bootstrap.yml文件,例如:
server:
port: 8071
spring:
application:
name: sca-auth
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
添加項目啟動類
package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ResourceAuthApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceAuthApplication.class, args);
}
}
啟動並訪問項目
項目啟動時,系統會默認生成一個登陸密碼,例如:


其中,默認用戶名為user,密碼為系統啟動時,在控制台呈現的密碼。執行登陸測試,登陸成功進入如下界面(因為沒有定義登陸頁面,所以會出現404):

自定義登陸邏輯
業務描述
我們的單點登錄系統最終會按照如下結構進行設計和實現,例如:

我們在實現登錄時,會在UI工程中,定義登錄頁面(login.html),然後在頁面中輸入自己的登陸賬號,登陸密碼,將請求提交給網關,然後網關將請求轉發到auth工程,登陸成功和失敗要返回json數據,在這個章節我們會按這個業務逐步進行實現
定義安全配置類
修改SecurityConfig配置類,添加登錄成功或失敗的處理邏輯,例如:
package com.jt.auth.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**初始化密碼加密對象*/
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**在這個方法中定義登錄規則
* 1)對所有請求放行(當前工程只做認證)
* 2)登錄成功信息的返回
* 3)登錄失敗信息的返回
* */
@Override
protected void configure(HttpSecurity http) throws Exception {
//關閉跨域工具
http.csrf().disable();
//放行所有請求
http.authorizeRequests().anyRequest().permitAll();
//登錄成功與失敗的處理
http.formLogin()
.successHandler(successHandler())
.failureHandler(failureHandler());
}
@Bean
public AuthenticationSuccessHandler successHandler(){
// return new AuthenticationSuccessHandler() {
// @Override
// public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//
// }
// }
return (request,response,authentication) ->{
//1.構建map對象,封裝響應數據
Map<String,Object> map=new HashMap<>();
map.put("state",200);
map.put("message","login ok");
//2.將map對象寫到客戶端
writeJsonToClient(response,map);
};
}
@Bean
public AuthenticationFailureHandler failureHandler(){
return (request,response, e)-> {
//1.構建map對象,封裝響應數據
Map<String,Object> map=new HashMap<>();
map.put("state",500);
map.put("message","login failure");
//2.將map對象寫到客戶端
writeJsonToClient(response,map);
};
}
private void writeJsonToClient(HttpServletResponse response,
Object object) throws IOException {
//1.將對象轉換為json
//將對象轉換為json有3種方案:
//1)Google的Gson-->toJson (需要自己找依賴)
//2)阿里的fastjson-->JSON (spring-cloud-starter-alibaba-sentinel)
//3)Springboot web自帶的jackson-->writeValueAsString (spring-boot-starter-web)
//我們這裡藉助springboot工程中自帶的jackson
//jackson中有一個對象類型為ObjectMapper,它內部提供了將對象轉換為json的方法
//例如:
String jsonStr=new ObjectMapper().writeValueAsString(object);
//3.將json字元串寫到客戶端
PrintWriter writer = response.getWriter();
writer.println(jsonStr);
writer.flush();
}
}
定義用戶信息處理對象
在spring security應用中底層會藉助UserDetailService對象獲取資料庫信息,並進行封裝,最後返回給認證管理器,完成認證操作,例如:
package com.jt.auth.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 登錄時用戶信息的獲取和封裝會在此對象進行實現,
* 在頁面上點擊登錄按鈕時,會調用這個對象的loadUserByUsername方法,
* 頁面上輸入的用戶名會傳給這個方法的參數
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
//UserDetails用戶封裝用戶信息(認證和許可權信息)
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
//1.基於用戶名查詢用戶信息(用戶名,用戶狀態,密碼,....)
//Userinfo userinfo=userMapper.selectUserByUsername(username);
String encodedPassword=passwordEncoder.encode("123456");
//2.查詢用戶許可權信息(後面會訪問資料庫)
//這裡先給幾個假數據
List<GrantedAuthority> authorities =
AuthorityUtils.createAuthorityList(//這裡的許可權信息先這麼寫,後面講
"sys:res:create", "sys:res:retrieve");
//3.對用戶信息進行封裝
return new User(username,encodedPassword,authorities);
}
}
網關中登陸路由配置
在網關配置文件中添加登錄路由配置,例如
- id: router02
uri: lb://sca-auth #lb表示負載均衡,底層默認使用ribbon實現
predicates: #定義請求規則(請求需要按照此規則設計)
- Path=/auth/login/** #請求路徑設計
filters:
- StripPrefix=1 #轉發之前去掉path中第一層路徑
基於Postman進行訪問測試
啟動sca-gateway,sca-auth服務,然後基於postman訪問網關,執行登錄測試,例如:

自定義登陸頁面
在sca-resource-ui工程的static目錄中定義登陸頁面,例如:
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>login</title>
</head>
<body>
<div class="container"id="app">
<h3>Please Login</h3>
<form>
<div class="mb-3">
<label for="usernameId" class="form-label">Username</label>
<input type="text" v-model="username" class="form-control" id="usernameId" aria-describedby="emailHelp">
</div>
<div class="mb-3">
<label for="passwordId" class="form-label">Password</label>
<input type="password" v-model="password" class="form-control" id="passwordId">
</div>
<button type="button" @click="doLogin()" class="btn btn-primary">Submit</button>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
var vm=new Vue({
el:"#app",//定義監控點,vue底層會基於此監控點在內存中構建dom樹
data:{ //此對象中定義頁面上要操作的數據
username:"",
password:""
},
methods: {//此位置定義所有業務事件處理函數
doLogin() {
//1.定義url
let url = "http://localhost:9000/auth/login"
//2.定義參數
let params = new URLSearchParams()
params.append('username',this.username);
params.append('password',this.password);
//3.發送非同步請求
axios.post(url, params).then((response) => {
debugger
let result=response.data;
console.log(result);
if (result.state == 200) {
alert("login ok");
} else {
alert(result.message);
}
})
}
}
});
</script>
</body>
</html>
啟動sca-resource-ui服務後,進入登陸頁面,輸入用戶名jack,密碼123456進行登陸測試。
頒發登陸成功令牌
構建令牌配置對象
本次我們藉助JWT(Json Web Token-是一種json格式)方式將用戶相關信息進行組織和加密,並作為響應令牌(Token),從服務端響應到客戶端,客戶端接收到這個JWT令牌之後,將其保存在客戶端(例如localStorage),然後攜帶令牌訪問資源伺服器,資源伺服器獲取並解析令牌的合法性,基於解析結果判定是否允許用戶訪問資源.
package com.jt.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
public class TokenConfig {
//定義簽名key,在執行令牌簽名需要這個key,可以自己指定.
private String SIGNING_KEY = "auth";
//定義令牌生成策略.
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
//定義Jwt轉換器,負責生成jwt令牌,解析令牌內容
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter=new JwtAccessTokenConverter();
//設置加密/解密口令
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
定義認證授權核心配置
第一步:在SecurityConfig中添加如下方法(創建認證管理器對象,後面授權伺服器會用到):
@Bean
public AuthenticationManager authenticationManagerBean()
throws Exception {
return super.authenticationManagerBean();
}
第二步:所有零件準備好了開始拼裝最後的主體部分,這個主體部分就是授權伺服器的核心配置
package com.jt.auth.config;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import java.util.Arrays;
/**
* 完成所有配置的組裝,在這個配置類中完成認證授權,JWT令牌簽發等配置操作
* 1)SpringSecurity (安全認證和授權)
* 2)TokenConfig
* 3)Oauth2(暫時不說)
*/
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer //開啟認證和授權服務
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
//此對象負責完成認證管理
private AuthenticationManager authenticationManager;
//TokenStore負責完成令牌創建,信息讀取
private TokenStore tokenStore;
//JWT令牌轉換器(基於用戶信息構建令牌,解析令牌)
private JwtAccessTokenConverter jwtAccessTokenConverter;
//密碼加密匹配器對象
private PasswordEncoder passwordEncoder;
//負責獲取用戶信息信息
private UserDetailsService userDetailsService;
//設置認證端點的配置(/oauth/token),客戶端通過這個路徑獲取JWT令牌
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
//配置認證管理器
.authenticationManager(authenticationManager)
//驗證用戶的方法獲得用戶詳情
.userDetailsService(userDetailsService)
//要求提交認證使用post請求方式,提高安全性
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET)
//要配置令牌的生成,由於令牌生成比較複雜,下面有方法實現
.tokenServices(tokenService());//這個不配置,默認令牌為UUID.randomUUID().toString()
}
//定義令牌生成策略
@Bean
public AuthorizationServerTokenServices tokenService(){
//這個方法的目標就是獲得一個令牌生成器
DefaultTokenServices services=new DefaultTokenServices();
//支持令牌刷新策略(令牌有過期時間)
services.setSupportRefreshToken(true);
//設置令牌生成策略(tokenStore在TokenConfig配置了,本次我們應用JWT-定義了一種令牌格式)
services.setTokenStore(tokenStore);
//設置令牌增強(允許設置令牌生成策略,默認是非jwt方式,現在設置為jwt方式,並在令牌Payload部分允許添加擴展數據,例如用戶許可權信息)
TokenEnhancerChain chain=new TokenEnhancerChain();
chain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
services.setTokenEnhancer(chain);
//設置令牌有效期
services.setAccessTokenValiditySeconds(3600);//1小時
//刷新令牌應用場景:一般在用戶登錄系統後,令牌快過期時,系統自動幫助用戶刷新令牌,提高用戶的體驗感
services.setRefreshTokenValiditySeconds(3600*72);//3天
return services;
}
//設置客戶端詳情類似於用戶詳情
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//客戶端id (客戶端訪問時需要這個id)
.withClient("gateway-client")
//客戶端秘鑰(客戶端訪問時需要攜帶這個密鑰)
.secret(passwordEncoder.encode("123456"))
//設置許可權
.scopes("all")//all只是個名字而已和寫abc效果相同
//允許客戶端進行的操作 這裡的認證方式表示密碼方式,裡面的字元串千萬不能寫錯
.authorizedGrantTypes("password","refresh_token");
}
// 認證成功後的安全約束配置,對指定資源的訪問放行,我們登錄時需要訪問/oauth/token,需要對這樣的url進行放行
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//認證通過後,允許客戶端進行哪些操作
security
//公開oauth/token_key端點
.tokenKeyAccess("permitAll()")
//公開oauth/check_token端點
.checkTokenAccess("permitAll()")
//允許提交請求進行認證(申請令牌)
.allowFormAuthenticationForClients();
}
}
配置網關認證的URL
- id: router02
uri: lb://sca-auth
predicates:
#- Path=/auth/login/** #沒要令牌之前,以前是這樣配置
- Path=/auth/oauth/** #微服務架構下,需要令牌,現在要這樣配置
filters:
- StripPrefix=1
Postman訪問測試
第一步:啟動服務
依次啟動sca-auth服務,sca-resource-gateway服務。
第二步:檢測sca-auth服務控制台的Endpoints信息,例如:

第三步:打開postman進行登陸訪問測試

登陸成功會在控制台顯示令牌信息,例如:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Mjk5OTg0NjAsInVzZXJfbmFtZSI6ImphY2siLCJhdXRob3JpdGllcyI6WyJzeXM6cmVzOmNyZWF0ZSIsInN5czpyZXM6cmV0cmlldmUiXSwianRpIjoiYWQ3ZDk1ODYtMjUwYS00M2M4LWI0ODYtNjIyYjJmY2UzMDNiIiwiY2xpZW50X2lkIjoiZ2F0ZXdheS1jbGllbnQiLCJzY29wZSI6WyJhbGwiXX0.-Zcmxwh0pz3GTKdktpr4FknFB1v23w-E501y7TZmLg4",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJqYWNrIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImFkN2Q5NTg2LTI1MGEtNDNjOC1iNDg2LTYyMmIyZmNlMzAzYiIsImV4cCI6MTYzMDI1NDA2MCwiYXV0aG9yaXRpZXMiOlsic3lzOnJlczpjcmVhdGUiLCJzeXM6cmVzOnJldHJpZXZlIl0sImp0aSI6IjIyOTdjMTg2LWM4MDktNDZiZi1iNmMxLWFiYWExY2ExZjQ1ZiIsImNsaWVudF9pZCI6ImdhdGV3YXktY2xpZW50In0.1Bf5IazROtFFJu31Qv3rWAVEtFC1NHWU1z_DsgcnSX0",
"expires_in": 3599,
"scope": "all",
"jti": "ad7d9586-250a-43c8-b486-622b2fce303b"
}
登陸頁面登陸方法設計
登陸成功以後,將token存儲到localStorage中,修改登錄頁面的doLogin方法,例如
doLogin() {
//1.定義url
let url = "http://localhost:9000/auth/oauth/token"
//2.定義參數
let params = new URLSearchParams()
params.append('username',this.username);
params.append('password',this.password);
params.append("client_id","gateway-client");
params.append("client_secret","123456");
params.append("grant_type","password");
//3.發送非同步請求
axios.post(url, params).then((response) => {
alert("login ok");
let result=response.data;
localStorage.setItem("accessToken",result.access_token);
location.href="/fileupload.html";
}).catch((error)=>{
console.log(error);
})
}
資源伺服器配置
添加依賴
打開資源服務的pom.xml文件,添加oauth2依賴。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
令牌處理器配置
package com.jt.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/**
* 創建JWT令牌配置類,基於這個類實現令牌對象的創建和解析.
* JWT令牌的構成有三部分構成:
* 1)HEADER (頭部信息:令牌類型,簽名演算法)
* 2)PAYLOAD (數據信息-用戶信息,許可權信息,令牌失效時間,...)
* 3)SIGNATURE (簽名信息-對header和payload部分進行加密簽名)
*/
@Configuration
public class TokenConfig {
//定義令牌簽發口令(暗號),這個口令自己定義即可
//在對header和PAYLOAD部分進行簽名時,需要的一個口令
private String SIGNING_KEY= "auth";
//初始化令牌生成策略(默認生成策略 UUID)
//這裡我們採用JWT方式生成令牌
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
//構建JWT令牌轉換器對象,基於此對象創建令牌,解析令牌
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter=new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
資源服務令牌解析配置
package com.jt.resource.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
/**
* token服務配置
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore);
}
/**
* 路由安全認證配置
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler());
http.authorizeRequests().anyRequest().permitAll();
}
//沒有許可權時執行此處理器方法
public AccessDeniedHandler accessDeniedHandler() {
return (request, response, e) -> {
Map<String, Object> map = new HashMap<>();
map.put("state", HttpServletResponse.SC_FORBIDDEN);//SC_FORBIDDEN的值是403
map.put("message", "沒有訪問許可權,請聯繫管理員");
//1設置響應數據的編碼
response.setCharacterEncoding("utf-8");
//2告訴瀏覽器響應數據的內容類型以及編碼
response.setContentType("application/json;charset=utf-8");
//3獲取輸出流對象
PrintWriter out=response.getWriter();
//4 輸出數據
String result=
new ObjectMapper().writeValueAsString(map);
out.println(result);
out.flush();
};
}
}
資源服務令牌解析配置
package com.jt.resource.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
/**
* token服務配置
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore);
}
/**
* 路由安全認證配置
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler());
http.authorizeRequests().anyRequest().permitAll();
}
//沒有許可權時執行此處理器方法
public AccessDeniedHandler accessDeniedHandler() {
return (request, response, e) -> {
Map<String, Object> map = new HashMap<>();
map.put("state", HttpServletResponse.SC_FORBIDDEN);//SC_FORBIDDEN的值是403
map.put("message", "沒有訪問許可權,請聯繫管理員");
//1設置響應數據的編碼
response.setCharacterEncoding("utf-8");
//2告訴瀏覽器響應數據的內容類型以及編碼
response.setContentType("application/json;charset=utf-8");
//3獲取輸出流對象
PrintWriter out=response.getWriter();
//4 輸出數據
String result=
new ObjectMapper().writeValueAsString(map);
out.println(result);
out.flush();
};
}
}
ResourceController 方法配置
在controller的上傳方法上添加 @PreAuthorize(「hasAuthority(『sys:res:create』)」)註解,用於告訴底層框架方法此方法需要具備的許可權,例如
@PreAuthorize("hasAuthority('sys:res:create')")
@PostMapping("/upload/")
public String uploadFile(MultipartFile uploadFile) throws IOException {
...
}
啟動服務訪問測試
- 第一步:啟動服務(sca-auth,sca-resource-gateway,sca-resource)
- 第二步:執行登陸獲取access_token令牌
- 第三步:攜帶令牌訪問資源(url中的前綴”sca”是在資源伺服器中自己指定的,你的網關怎麼配置的,你就怎麼寫)
設置請求頭(header),要攜帶令牌並指定請求的內容類型,例如

設置請求體(body),設置form-data,key要求為file類型,參數名與你服務端controller文件上傳方法的參數名相同,值為你選擇的文件,例如

上傳成功會顯示你訪問文件需要的路徑,假如沒有許可權會提示你沒有訪問許可權。
文件上傳JS方法設計
function upload(file){
//定義一個表單
let form=new FormData();
//將圖片添加到表單中
form.append("uploadFile",file);
let url="http://localhost:9000/sca/resource/upload/";
//非同步提交方式1
axios.post(url,form,{headers:{"Authorization":"Bearer "+localStorage.getItem("accessToken")}})
.then(function (response){
let result=response.data;
if(result.state==403){
alert(result.message);
return;
}
alert("upload ok");
})
}
技術摘要應用實踐說明
背景分析
企業中數據是最重要的資源,對於這些數據而言,有些可以直接匿名訪問,有些只能登錄以後才能訪問,還有一些你登錄成功以後,許可權不夠也不能訪問.總之這些規則都是保護系統資源不被破壞的一種手段.幾乎每個系統中都需要這樣的措施對數據(資源)進行保護.我們通常會通過軟體技術對這樣業務進行具體的設計和實現.早期沒有統一的標準,每個系統都有自己獨立的設計實現,但是對於這個業務又是一個共性,後續市場上就基於共性做了具體的落地實現,例如Spring Security,Apache shiro,JWT,Oauth2等技術誕生了.
Spring Security 技術
Spring Security 是一個企業級安全框架,由spring官方推出,它對軟體系統中的認證,授權,加密等功能進行封裝,並在springboot技術推出以後,配置方面做了很大的簡化.現在市場上分散式架構中的安全控制,正在逐步的轉向Spring Security。Spring Security 在企業中實現認證和授權業務時,底層構建了大量的過濾器,如圖所示:

其中:
圖中綠色部分為認證過濾器,黃色部分為授權過濾器。Spring Security就是通過這些過濾器然後調用相關對象一起完成認證和授權操作.
Jwt 數據規範
JWT(JSON WEB Token)是一個標準,採用數據自包含方式進行json格式數據設計,實現各方安全的信息傳輸,其官方網址為:https://jwt.io/。官方JWT規範定義,它構成有三部分,分別為Header(頭部),Payload(負載),Signature(簽名),其格式如下:
xxxxx.yyyyy.zzzzz
Header 部分是一個 JSON 對象,描述 JWT 的元數據,通常是下面的樣子。
{
"alg": "HS256",
"typ": "JWT"
}
上面代碼中,alg屬性表示簽名的演算法(algorithm),默認是 HMAC SHA256(簡寫HS256);typ屬性表示這個令牌(token)的類型(type),JWT 令牌統一寫為JWT。最後,將這個 JSON 對象使用 Base64URL 演算法(詳見後文)轉成字元串。
Payload部分
Payload 部分也是一個 JSON 對象,用來存放實際需要傳遞的數據。JWT規範中規定了7個官方欄位,供選用。
- iss (issuer):簽發人
- exp (expiration time):過期時間
- sub (subject):主題
- aud (audience):受眾
- nbf (Not Before):生效時間
- iat (Issued At):簽發時間
- jti (JWT ID):編號
- 除了官方欄位,你還可以在這個部分定義私有欄位,下面就是一個例子。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
注意,JWT 默認是不加密的,任何人都可以讀到,所以不要把秘密信息放在這個部分。
這個 JSON 對象也要使用 Base64URL 演算法轉成字元串。
Signature部分
Signature 部分是對前兩部分的簽名,其目的是防止數據被篡改。
首先,需要指定一個密鑰(secret)。這個密鑰只有伺服器才知道,不能泄露給用戶。然後,使用 Header 裡面指定的簽名演算法(默認是 HMAC SHA256),按照下面的公式產生簽名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出簽名以後,把 Header、Payload、Signature 三個部分拼成一個字元串,每個部分之間用”點”(.)分隔,就可以返回給用戶。
Oauth2規範
oauth2定義了一種認證授權協議,一種規範,此規範中定義了四種類型的角色:
1)資源有者(User)
2)認證授權伺服器(jt-auth)
3)資源伺服器(jt-resource)
4)客戶端應用(jt-ui)
同時,在這種協議中規定了認證授權時的幾種模式:
1)密碼模式 (基於用戶名和密碼進行認證)
2)授權碼模式(就是我們說的三方認證:QQ,微信,微博,。。。。)
3)…
總結(Summary)
重難點分析
- 單點登陸系統的設計架構(微服務架構)
- 服務的設計及劃分(資源伺服器,認證伺服器,網關伺服器,客戶端服務)
- 認證及資源訪問的流程(資源訪問時要先認證再訪問)
- 認證和授權時的一些關鍵技術(Spring Security,Jwt,Oauth2)
FAQ 分析
- 為什麼要單點登陸(分散式系統,再訪問不同服務資源時,不要總是要登陸,進而改善用戶體驗)
- 單點登陸解決方案?(市場常用兩種: spring security+jwt+oauth2,spring securit+redis+oauth2)
- Spring Security 是什麼?(spring框架中的一個安全默認,實現了認證和授權操作)
- JWT是什麼?(一種令牌格式,一種令牌規範,通過對JSON數據採用一定的編碼,加密進行令牌設計)
- OAuth2是什麼?(一種認證和授權規範,定義了單點登陸中服務的劃分方式,認證的相關類型)
…
Bug 分析
- 401 : 訪問資源時沒有認證。
- 403 : 訪問資源時沒有許可權。
- 404:訪問的資源找不到(一定要檢查你訪問資源的url)
- 405: 請求方式不匹配(客戶端請求方式是GET,服務端處理請求是Post就是這個問題)
- 500: 不看後台無法解決?(error,warn)
- …
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/209229.html