文章目录
- 学习链接
- 单点登录
- cookie+session&token模式&前端无感token刷新&双token模式&黑名单
- auth-server
- 引入依赖
- application.yml
- AuthServerConfig
- SecurityConfig
- UserController
- login.html
- client1
- 引入依赖
- application.yml配置文件
- SecurityConfig
- Client1Application
- HaloController
- index.html
- client2
- 引入依赖
- application.yml配置文件
- SecurityConfig
- Client2Application
- HaloController
- index.html
学习链接
cookie & session & localStorage & sessionStorage - 自己的
session共享原理 - 自己的
spring-session学习 - 自己的
OAuth2流程和源码解析 - 自己的 - 有道笔记
Security OAuth2 授权 & JWT - 自己的
Spring Security整合Gitee第三方登录 - 自己的
JWT使用 - 自己的
token无感刷新【渡一教育】
单点登录的模式【渡一教育】
Spring Boot+OAuth2,一个注解搞定单点登录!
SpringCloud微服务实战——搭建企业级开发框架(四十):使用Spring Security OAuth2实现单点登录(SSO)系统
Spring Security Oauth2 SSO单点登录配置及原理深度剖析
多系统单点登录(sso)设计
Java-前后端分离-单点登录(SSO二级跨域和跨一级域名)
【学习笔记】:基于SpringBoot实现单点登录sso测试demo
springsecurity实现单点登录
主要是体会下,security是如何考虑单点登录的,以及它的具体做法:全部代码 - gitee
单点登录
cookie+session&token模式&前端无感token刷新&双token模式&黑名单
当1个公司开发了多个系统时,如系统A,系统B,系统C。用户在登录A系统时,需要跳转到B系统时,在B系统又要去登录一遍,这无疑时是比较麻烦的。
这时候,就可以抽离出1个统一认证中心服务专门处理用户认证。首先,当用户访问系统A时,由于用户没有登录,所以会跳转到统一认证中心登录,当用户登录成功后,在统一认证中心下可以使用cookie + session的机制保存用户的登录信息,然后用户跳转到系统A,并且携带登录统一认证中心成功的凭证(等等,这里面可能会涉及到cookie的不可跨域名性,就算存在localStorage里面也是有着隔离,这些是具体的细节,暂不考虑,实现时再说),这样系统A就收到了用户发过来的凭证,然而系统A却不能鉴定该凭证的真实性以及该凭证所代表的用户信息,因此系统A需要发送请求给认证中心获取该凭证所对应的用户数据(等等,如果认证服务与系统A在浏览器都会携带同1个cookie时,是可以用spring-session实现会话共享的,也就不用发请求了。补充:访问这2个服务能不能携带同1个cookie是有条件的)。系统A通过发请求给认证中心校验凭证有效时,就会在凭证允许的权限范围内给用户访问。此时,当用户访问系统B时,要仍然携带凭证(等等,此时跳转到系统B,这个凭证由于不可跨域名性,不一定能带上,可以先跳回认证中心,因为已经在认证中心登录过,再跳转到系统B,并携带凭证),系统B也同样需要发请求给认证中心,校验凭证的真实性和用户信息,如果没有问题,则允许用户访问。
上面实现的大致逻辑就是这样,里面存在1个问题:如果某个系统,比如系统C比较热门,用户比较多的情况下,比如:200万用户都活跃,这样认证中心就得统一管理这么多已登录用户的信息,并且每一次用户访问系统C都需要访问认证中心来获取用户信息。那么对于认证中心来说,1个是存储用户信息问题(这个可以用redis解决),1个是处理大量校验凭证请求的问题,这样就很有可能导致认证中心不堪重负,假设此时认证中心挂了,那么所有的子系统就全都报废了(这个就比较严重了,各个子系统都依赖这个认证中心才能正常工作)。但是,这样集中式的管理用户会话数据,会比较方便对用户进行控制,比如让用户下线只需要删除这个会话即可,那么用户会立即需要重新认证登录。
对于上面的问题,使用jwt令牌可以解决,当用户在统一认证中心登录成功时,生成1个jwt令牌,这个jwt令牌由3部分组成:1个令牌类型和算法,1个令牌内容可base64解码,1个签名可验签。用户登录成功后,保存此令牌。然后,拿着这个令牌去访问系统A,因为jwt令牌上已经携带的内容包含了用户信息,所以系统A可以直接读取这个jwt令牌中的用户信息,并且可以验证签名来判断jwt令牌的内容是否被篡改过,如果没有被篡改过,并且这个jwt令牌的失效时间还长,并没有过期。那么系统A就直接给用户返回内容即可。此时,用户访问系统C也是一样携带这个令牌。(至于跳转系统C的时候,是怎么携带这个令牌,具体实现的时候再说,反正就是这么个意思)
上面使用jwt的token方式就是这样,认证中心不再保存已登录用户的信息,而是给了登录用户1个可识别和可验签的代表用户信息的令牌。认证中心也不再处理校验凭证的请求了。即便认证中心挂掉了,此令牌仍然可在各子系统中使用。这样也会带来一些问题:不能对用户进行方便控制,不好做到让用户立即下线。令牌一旦颁发出去,那么想撤销这个令牌就相对来说没那么容易了。虽然,也可以采用黑名单的方式,每个子系统在redis中维护着自己黑名单(各个子系统不一定使用的是同一个redis),但撤销还得通知到其它系统。
jwt令牌减轻了认证中心的压力,但带来了撤销令牌的麻烦。因此,OAuth2中就涉及到了1个刷新令牌的机制。既然令牌不能立即撤销,那么给这个令牌1个比较短的过期时间,比如:2个小时。也就是,用户登录后,会获得2个令牌,1个访问令牌,1个刷新令牌。用户使用访问令牌访问各个子系统,但因为访问令牌是有时间限制的,一旦到了过期时间,访问令牌就被各个子系统识别出来已经过期了,无法使用。此时,再让用户登录一遍的话,就也显得麻烦了,因此就让用户需要拿着刷新令牌(这个刷新令牌的有效时间比访问令牌的时间要长,比如:7天或1个月,)去认证中心换取新的访问令牌,然后拿着这个新的访问令牌去访问各个子系统。这样,用户每隔一段比较短的时间,就得拿着刷新令牌获取访问令牌。如果此时要控制用户的话,等到用户下一次获取访问令牌时,不给他刷新就行了,那么用户就只得重新登录了,只是没有那么即时了。
如果需要即时性,那么就由认证服务给各子系统通知一下(这个通知接口也得做权限控制),更新各子系统存在redis中的黑名单,黑名单记录用户的id(因为令牌颁发后,不会记录该令牌具体的值,否则跟在会话中记录用户用户数据没啥区别了,同时也是内存负担)存储个2小时,那么这2个小时内,用户使用旧的访问令牌是无法访问的,当用户使用刷新令牌获取新的访问令牌时,这时候就可以决定是否颁发新的令牌,如果颁发新的访问令牌,则通知各个子系统从黑名单中主动移除。
还有1个比较严重的问题需要考虑下,如果黑名单只是记录用户的id的话,可能还是不够。假设用户登录了1次之后,获取了访问令牌和刷新令牌,持有访问令牌在令牌有效期内可以任意访问用户资源,持有刷新令牌同样在有效期内可重新获取刷新令牌。那么如果用户修改了密码的情况下,它们在有效期内仍然能够使用,令牌的重要性甚至上升到了与用户密码凭据同样的高度。同样,用户在多个设备而登录多次时,那么就会存在1个用户有多个访问令牌和多个刷新令牌的存在。因此,登录成功后,最好是将用户id和对应的访问令牌和刷新令牌的JTI属性(令牌唯一标识)关系也给保存下来。当用户修改密码时,找到所有之前的访问令牌id,把它们通知给各个子系统,让它们写到黑名单中,找到所有之前的刷新令牌id,然后把他们写到认证服务中心的黑名单中,并且也可以做到最多允许同时在线客户端数量,根据设置确定踢人模式。而一旦用户修改密码,则将之前颁发给该用户的所有访问令牌的JTI标识写到所有子系统,同时,把用户对应的刷新令牌写到认证中心黑名单。
auth-server
引入依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.6.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>org.javaboy</groupId><artifactId>auth-server</artifactId><version>0.0.1-SNAPSHOT</version><name>auth-server</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version><spring-cloud.version>Hoxton.SR3</spring-cloud.version></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-oauth2</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
application.yml
server:port: 1110
AuthServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {@AutowiredPasswordEncoder passwordEncoder;@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security.checkTokenAccess("permitAll()");}@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("c1").secret(passwordEncoder.encode("secret")).autoApprove(true).redirectUris("http://localhost:1111/login", "http://localhost:1112/login").scopes("user").accessTokenValiditySeconds(7200).authorizedGrantTypes("authorization_code");}
}
SecurityConfig
@Configuration
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@BeanPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/login.html", "/css/**", "/js/**", "/images/**", "/favicon.ico");}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.requestMatchers().antMatchers("/login").antMatchers("/oauth/authorize").and().authorizeRequests().anyRequest().authenticated().and().formLogin().loginPage("/login.html").loginProcessingUrl("/login").permitAll().and().csrf().disable();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.inMemoryAuthentication().withUser("zzhua").password(passwordEncoder().encode("zzhua")).roles("admin");}
}
UserController
@RestController
public class UserController {@GetMapping("/user")public Principal getCurrentUser(Principal principal) {return principal;}
}
login.html
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8"><title>Document</title>
</head>
<body>
<div style="overflow: hidden"><div class="login-form"><h1>统一认证平台</h1><form action="/login" method="post"><div class="login-item"><span class="login-item-name">用户名: </span><input type="text" name="username"/></div><div class="login-item"><span class="login-item-name">密码: </span><input type="text" name="password"/></div><div class="login-item submit"><input type="submit" value="登录"/></div></form></div>
</div>
</body>
<style>* {box-sizing: border-box;}body {overflow: hidden;margin: 0;}h1 {margin-left: 25px;}.login-form {width: 600px;margin: 120px auto;text-align: center;border: 1px solid #eee;border-radius: 5px;box-shadow: 2px 9px 6px 0px #ddd;}.login-form form {width: 60%;margin: 20px auto;padding: 10px 0 50px;}.login-item {display: flex;margin-bottom: 20px;height: 26px;line-height: 32px;}.login-item .login-item-name {width: 100px;text-align: right;padding-right: 15px;}.login-item input {flex: 1;outline: none;border: 1px solid #ddd;height: 32px;font-size: 18px;font-weight: lighter;padding-left: 10px;border-radius: 5px;}.login-item.submit {padding-left: 100px;}.login-item.submit input[type=submit]{border: none;background-color: #86bdcf;letter-spacing: 10px;color: #ffffff;transition: all .2s;}.login-item.submit input[type=submit]:hover {background-color: #6da0b2;}</style>
</html>
client1
引入依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.6.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>org.javaboy</groupId><artifactId>client1</artifactId><version>0.0.1-SNAPSHOT</version><name>client1</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version><spring-cloud.version>Hoxton.SR3</spring-cloud.version></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-oauth2</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
application.yml配置文件
server:port: 1111servlet:session:cookie:name: s1
security:oauth2:client:client-id: c1client-secret: secretuser-authorization-uri: http://localhost:1110/oauth/authorizeaccess-token-uri: http://localhost:1110/oauth/tokenresource:user-info-uri: http://localhost:1110/user
SecurityConfig
@Configuration
@EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().csrf().disable();}
}
Client1Application
@SpringBootApplication
public class Client1Application {public static void main(String[] args) {SpringApplication.run(Client1Application.class, args);}}
HaloController
@RestController
public class HaloController {@GetMapping("/halo")public String halo() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();System.out.println(authentication.getName() + Arrays.toString(authentication.getAuthorities().toArray()));return "halo-1111";}
}
index.html
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8"><title>Document</title>
</head>
<body><h1>【client1】</h1><a href="http://localhost:1112">跳转到client2</a><button id="btn1">获取client1中的数据</button><button id="btn2">获取client2中的数据</button></body>
<script>let btn1 = document.querySelector("#btn1")let btn2 = document.querySelector("#btn2")btn1.addEventListener('click', function () {fetch("http://localhost:1111/halo")})btn2.addEventListener('click', function () {fetch("http://localhost:1112/halo")})
</script>
</html>
client2
引入依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.6.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>org.javaboy</groupId><artifactId>client2</artifactId><version>0.0.1-SNAPSHOT</version><name>client2</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version><spring-cloud.version>Hoxton.SR3</spring-cloud.version></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-oauth2</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
application.yml配置文件
server:port: 1112servlet:session:cookie:name: s2
security:oauth2:client:client-id: c1client-secret: secretuser-authorization-uri: http://localhost:1110/oauth/authorizeaccess-token-uri: http://localhost:1110/oauth/tokenresource:user-info-uri: http://localhost:1110/user
SecurityConfig
@Configuration
@EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().csrf().disable();}
}
Client2Application
@SpringBootApplication
public class Client2Application {public static void main(String[] args) {SpringApplication.run(Client2Application.class, args);}}
HaloController
@RestController
public class HaloController {@CrossOrigin@GetMapping("/halo")public String halo() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();System.out.println(authentication.getName() + Arrays.toString(authentication.getAuthorities().toArray()));return "halo";}
}
index.html
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8"><title>Document</title>
</head>
<body><h1>【client2】</h1><a href="http://localhost:1111">跳转到client1</a><button id="btn1">获取client1中的数据</button><button id="btn2">获取client2中的数据</button></body>
<script>let btn1 = document.querySelector("#btn1")let btn2 = document.querySelector("#btn2")btn1.addEventListener('click', function () {fetch("http://localhost:1111/halo")})btn2.addEventListener('click', function () {fetch("http://localhost:1112/halo")})
</script>
</html>