一、基本概念
1.1 認證方式
1.1.1 基於session方式認證
他的流程是:用戶認證成功后,服務端生成相應的用戶數據保存在session中,發給客戶端的session_id保存在cookie中。這樣用戶請求時只要帶上session_id就可以驗證服務端是否存在session,以此完成用戶的校驗。當用戶退出系統或session過期時,客戶端的session_id也就無效了。
1.1.2 基於token認證方式
他的流程是:用戶認證成功后,服務端生成一個token發給客戶端,客戶端放到cookie或localStorage等存儲中,每次請求帶上token,服務端收到后就可以驗證。
1.2 什么是授權
授權:用戶認證通過后根據用戶的權限來控制用戶訪問資源的過程。
1.3 權限模型
最簡單權限表設計。
二、快速入門
2.1 用戶認證
先自行搭建一個SpringMvc或者SpringBoot項目.
2.1.1 引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.1.2 配置類
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置用戶信息服務
* @return
*/
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager=new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}
/**
* 密碼編碼器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
/**
* 安全攔截機制
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/r/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.successForwardUrl("/login-success");
}
}
2.1.3 測試資源訪問
寫一個controller進行測試.
@RestController
public class ResourceController {
@RequestMapping("/r/r1")
public String r1(){
return "訪問資源1";
}
@RequestMapping("/r/r2")
public String r2(){
return "訪問資源2";
}
}
直接訪問http://localhost:8080/r/r2,會跳到登陸頁面,登陸成功后訪問則成功.
以上就利用SpringSecurity完成來了認證功能.
2.2 資源控制
只需在antMatchers("/r/r1").hasAnyAuthority("p1")方法上加上hasAnyAuthority就可以了.
這個方法代表要訪問/r/r1,必須得有p1權限.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/r/r1").hasAnyAuthority("p1")
.antMatchers("/r/r2").hasAnyAuthority("p2")
.anyRequest().permitAll()
.and()
.formLogin()
.successForwardUrl("/login-success");
}
注意:規則的順序很重要,具體的規則要放在最上面,permitAll這種放在下面
三、工作原理
Spring Security對資源對保護是通過filter來實現對,當初始化Spring Security時,會創建一個名為SpringSecurityFilterChain的Servlet過濾器,類型為FilterChainProxy,他實現了javax.servlet.Filter接口,因此外部的請求會經過此類.
SpringSecurity的功能主要是通過過濾器鏈來完成的.
下面介紹幾個主要的攔截器:
- SecurityContextPersistenceFilter:整個攔截過程的入口和出口
- UsernamePasswordAuthenticationFilter:用於處理來自表單提交的認證
- FilterSecurityInterceptor:用於保護web資源的
- ExceptionTranslationFilter:能夠捕獲FilterChain的所有異常並處理.
認證過程:
3.1 改為從數據庫查詢用戶
實現UserDetailsService接口
@Service
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//這里可以寫從數據庫查的邏輯
UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
return userDetails;
}
}
3.2 加密后的密碼校對
先將密碼加密器改為BCryptPasswordEncoder
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
加密算法的使用
public static void main(String[] args) {
//生成加鹽的密碼
String hashpw = BCrypt.hashpw("123456", BCrypt.gensalt());
//校驗密碼
boolean checkpw = BCrypt.checkpw("123456", hashpw);
System.out.print(checkpw);
}
3.3 權限認證
授權流程:
AccessDecisionManager采用投票的方式來確定是否能夠訪問對應受保護的資源.
默認的實現是AffirmativeBased類
四、自定義頁面
4.1 自定義登陸頁面
package com.mmc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密碼編碼器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 安全攔截機制
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//關閉csrf
http.csrf().disable().
authorizeRequests()
.antMatchers("/r/r1").hasAnyAuthority("p1")
.antMatchers("/r/r2").hasAnyAuthority("p2")
.anyRequest().permitAll()
.and()
.formLogin()
//登陸頁面
.loginPage("/loginPage")
//登陸請求的url .loginProcessingUrl("/userlogin")
.successForwardUrl("/login-success");
}
}
定義一個登陸頁面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陸頁</title>
</head>
<body>
<form action="/userlogin" method="post">
<p>用戶名:<input name="username" type="text"> </p>
<p>密碼:<input name="password" type="text"></p>
<button type="submit">登陸</button>
</form>
</body>
</html>
4.2 會話控制
4.2.1 獲取當前用戶信息
public String getUserInfo(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//用戶身份
Object principal = authentication.getPrincipal();
if(principal==null){
return "";
}
if(principal instanceof UserDetails){
UserDetails userDetails = (UserDetails) principal;
return userDetails.getUsername();
}else {
return principal.toString();
}
}
4.2.2 會話控制
我們可以通過下列選項控制會話何時創建及如何與SpringSecurity交互
機制 | 描述 |
---|---|
always | 沒有session存在就創建一個 |
ifRequired | 如果有需要就創建一個登陸時(默認) |
never | SpringSecurity不會創建session,但是應用其他地方創建來的話,可以使用 |
stateless | 不創建不使用 |
配置地方如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().
authorizeRequests()
.antMatchers("/r/r1").hasAnyAuthority("p1")
.antMatchers("/r/r2").hasAnyAuthority("p2")
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/loginPage")
.loginProcessingUrl("/userlogin")
.successForwardUrl("/login-success")
.and()
//控制器
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
}
4.3 自定義登出
可以配置如下選項:
.and()
.logout()
.logoutSuccessUrl("/login-view")
.addLogoutHandler(logoutHandle)
.logoutSuccessHandler(logoutSuccessHandler);
4.4 授權
4.4.1 web方式授權
http.csrf().disable().
authorizeRequests()
.antMatchers("/r/r1").hasAnyAuthority("p1")
.antMatchers("/r/r2").hasAnyAuthority("p2")
4.4.2 方法授權
- 配置類上加注解
@EnableGlobalMethodSecurity(securedEnabled = true)
- 方法上加注解
@RequestMapping("/saveUser")
@ResponseBody
@PreAuthorize("hasAuthrity('p1')")
public String saveUser(){
User user=new User();
user.setUsername("zhangsan");
user.setPassword(BCrypt.hashpw("123456",BCrypt.gensalt()));
user.setMobile("18380430770");
userMapper.insert(user);
return "sucess";
}
五、分布式系統認證方案
5.1 分布式認證需求
統一的認證授權
提供獨立的認證服務,統一處理認證授權.無論上不同類型的用戶,還是不同類型的客戶端(web、app),均采用一致的認證、權限、會話機制,實現統一授權.
應用接入認證
應提供擴展和開放能力,提供安全的系統對接機制,並可開放部分API給 第三方使用.
5.2 分布式方案選型
5.2.1 采用session的方式
優點:安全、傳輸數據量小
缺點:分布式應用中需要同步session、session上基於coockie的,有的客戶端不支持coockie
session處理的三個方法:
- session同步
- session黏貼,即用戶去某服務器登陸,那么他的所有請求就都路由到指定服務器
- session統一存儲.
5.2.2 采用token的方式
優點:第三方更適合接入,可使用當前流行的開放協議OAuth2.0和JWT
缺點:token中包含用戶信息,數據大,帶寬壓力大、token檢驗需要耗費CPU
六、OAuth2.0
6.1 概念介紹
OAuth是一個開放標准,允許用戶授權第三方應用訪問存儲在另外的服務器上的信息,而不用提供用戶名或密碼給第三方應用.
第三方登陸流程圖:
OAuth2.0角色介紹:
- 客戶端
包括安卓客戶端、瀏覽器、小程序等
2. 資源擁有者
通常是用戶,也可以是應用程序
3. 認證服務器
用於服務提供商對資源擁有的身份進行認證、對訪問資源進行授權.認證成功后發放令牌,作為訪問資源服務器的憑證.
- 資源服務器
存儲資源的服務器.
問題:
服務提供商會讓所有的客戶端接入到他的授權服務器嗎?答案是不能.他會給准入的接入方一個身份:
- client_id:客戶端標識
- client_secret:客戶端密鑰
6.2 環境搭建
6.2.1 創建項目
先自行創建一個springcloud微服務項目.父工程的pom文件為:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modules>
<module>spring-security-uaa</module>
<module>spring-security-order</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
</parent>
<groupId>com.mmc</groupId>
<artifactId>spring-cloud-security-study</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>mysql</groupId>-->
<!-- <artifactId>mysql-connector-java</artifactId>-->
<!-- <version>5.1.47</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
再在里面創建一個授權服務的module,pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-security-study</artifactId>
<groupId>com.mmc</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-uaa</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
</project>
6.2.2 授權服務器配置
- 配置客戶端詳細信息
@Service
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails userDetails = User.withUsername(username).password("$2a$10$R5vdYffOXhN2ay0Cke9YIezhlEzHaMt4i8Ndl9GXTOQepSp8ixpVy").authorities("p1").build();
return userDetails;
}
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("c1")
.secret(new BCryptPasswordEncoder().encode("secret"))
//資源列表
.resourceIds("res1")
//授權類型
.authorizedGrantTypes("authorization_code","password","client_credentials","implicit","refresh_token")
//允許的授權范圍,all是自定義的字符串
.scopes("all")
//false代表跳轉到授權頁面
.autoApprove(false)
//驗證回調地址
.redirectUris("http://www.baidu.com");
}
- 管理令牌
@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore(){
return new InMemoryTokenStore();
}
}
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices services=new DefaultTokenServices();
//客戶端信息
services.setClientDetailsService(clientDetailsService);
//是否產生刷新令牌
services.setSupportRefreshToken(true);
//令牌存儲策略
services.setTokenStore(tokenStore);
//令牌存活時間
services.setAccessTokenValiditySeconds(60*5);
services.setRefreshTokenValiditySeconds(60*10);
return services;
}
- 令牌訪問端點配置
package com.mmc.uaa.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密碼編碼器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 認證管理器
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
/**
* 安全攔截機制
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().
authorizeRequests()
.antMatchers("/r/r1")
.hasAnyAuthority("p1")
.antMatchers("/login*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
}
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
//基於內存的授權碼模式
return new InMemoryAuthorizationCodeServices();
}
/**
* 令牌訪問端點配置
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.
//密碼模式需要
authorizationCodeServices(authorizationCodeServices)
//授權碼模式需要
.authenticationManager(authenticationManager)
.tokenServices(tokenServices())
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
- 令牌訪問端點安全配置
/**
* 令牌訪問端點安全配置
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
- 框架默認的url鏈接
- /oauth/authorize 授權端點
- /oauth/token 獲取token
- /oauth/conirm_access 用戶確認授權提交端點
- /oauth/error 授權服務錯誤信息
- /oauth/check_token 提供給資源服務使用的令牌解析端點
- /oauth/token_key 提供公有密鑰的端點,如果你使用JWT令牌
6.2.3 授權模式
- 授權碼模式
步驟1:獲取code
請求示例:
登陸之后回跳到授權頁面,點擊允許后,會跳轉到redirect_url,並顯示出code
步驟2:獲取token(注意如果請求方式配了POST就要用POST方式)
即可獲取到token
授權碼模式是四種模式中最安全的模式.一般用於client是web服務端應用或第三方原生app調用資源服務的時候.
- 簡化模式
步驟1:直接拿token
一般來說簡化模式用於沒有服務器應用的第三方單頁面應用,因為沒有服務器就沒法接收授權碼.
- 密碼模式
步驟1:
這種模式非常簡單,但是卻會將用戶信息泄露給client,因此只能用於client是我們自己開發的情況.
- 客戶端模式
步驟1:
http://localhost:8080/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
6.3 JWT令牌
6.3.1 JWT簡介
Json web token(JWT)是一個開放的行業標准.定義了一種簡潔的,自包含的協議格式.用於在通信雙方傳遞json對象,傳遞的信息經過數字簽名可以被驗證和信任.JWT可以使用HMAC或RSA簽名,防止篡改.
JWT的優點:
- 基於json,方便解析
- 可以自定義內容,方便擴展
- 通過非對稱加密算法及簽名,安全性高
- 資源服務使用JWT可以不依賴認證服務即可完成授權.
JWT由以下三部分組成,每部分中間用.分割.如xxx.yyy.zzz
- header的部分
包括令牌的類型及使用的加密算法.
{
"alg":"HS256",
"typ":"JWT"
}
將上面的內容進行base64Url編碼,得到一個字符串就是JWT的第一部分.
- Payload
第二部分是負載,內容也是json對象,它是存放有效信息的地方,可以存JWT的現有字段,也可以自定義字段.此部分不建議放敏感信息,因為可以被解碼.最后將上面的內容進行base64Url編碼,得到一個字符串就是JWT的第二部分.
例子:
{
"merchantid":123,
"name":"wang"
}
- Signature
第三部分是簽名,防止內容被篡改.
例子:
HMACSH256(
base64UrlEncode(header)+.base64UrlEncode(payload),secret
)
secret:簽名使用的密鑰.
6.3.2 配置JWT
@Configuration
public class TokenConfig {
public static final String SIGN_KEY = "abc123";
@Bean
public JwtAccessTokenConverter tokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter=new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGN_KEY);
return jwtAccessTokenConverter;
}
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(tokenConverter());
}
}
然后在配置生成令牌的地方,加一段增強令牌的代碼:
/**
* 令牌管理服務
* @return
*/
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices services=new DefaultTokenServices();
//客戶端信息
services.setClientDetailsService(clientDetailsService);
//是否產生刷新令牌
services.setSupportRefreshToken(true);
//令牌存儲策略
services.setTokenStore(tokenStore);
//令牌存活時間
services.setAccessTokenValiditySeconds(60*5);
services.setRefreshTokenValiditySeconds(60*10);
//令牌增強
TokenEnhancerChain tokenEnhancerChain=new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
services.setTokenEnhancer(tokenEnhancerChain);
return services;
}