搞懂单点登录SSO,基于SpringBoot+JWT实现单点登录解决方案
在 Spring Boot 中实现单点登录(Single Sign-On, SSO)通常涉及与第三方身份提供商(如 OAuth2、OpenID Connect、SAML 等)的集成。以下是几种常见的单点登录对接方案及其实现方法:
Spring Security和OAuth2在功能和使用场景上有显著区别。
功能和用途
- Spring Security:主要用于身份认证和访问控制。它提供了基于角色的访问控制(RBAC)功能,确保用户只能访问他们被授权的资源。Spring Security通过注解和配置文件来简化安全配置,支持多种认证方式,如用户名密码、JWT等12。
- OAuth2:主要用于授权和认证。OAuth2是一个开放标准协议,允许用户授权第三方应用访问他们在某个服务提供商上的信息,而无需共享用户名和密码。OAuth2定义了四种授权模式:授权码模式、简化模式、密码模式和客户端模式3。
集成方式和应用场景
- Spring Security:通常与Spring Boot集成,用于构建安全的应用程序。它通过注解和配置文件来简化安全配置,支持多种认证方式。Spring Security可以与OAuth2结合使用,通过spring-security-oauth2模块实现OAuth2的认证和授权功能12。
- OAuth2:常用于需要第三方登录的场景,如社交媒体登录、API访问控制等。OAuth2可以与Spring Security集成,通过spring-security-oauth2库来实现具体的认证和授权逻辑。OAuth2的四种授权模式提供了灵活的授权方式,适用于不同的业务需求3。
总结
Spring Security和OAuth2在功能和用途上有明显的区别。Spring Security主要用于身份认证和访问控制,而OAuth2主要用于授权和认证。两者可以结合使用,通过spring-security-oauth2模块实现OAuth2的认证和授权功能,适用于需要安全控制和第三方登录的应用场景。
1. 基于 OAuth2 的单点登录
OAuth2 是一种授权框架,常用于实现单点登录。Spring Boot 提供了对 OAuth2 的良好支持。
1.1 使用 Spring Security OAuth2
-
添加依赖:
在pom.xml
中添加 Spring Security OAuth2 依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency>
-
配置 OAuth2 客户端:
在application.yml
中配置 OAuth2 客户端信息:spring: security: oauth2: client: registration: google: client-id: <your-client-id> client-secret: <your-client-secret> scope: openid,profile,email redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" provider: google: authorization-uri: https://accounts.google.com/o/oauth2/auth token-uri: https://oauth2.googleapis.com/token user-info-uri: https://openidconnect.googleapis.com/v1/userinfo user-name-attribute: sub
-
配置 Spring Security:
在 Spring Security 配置类中启用 OAuth2 登录:@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .oauth2Login(); } }
-
获取用户信息:
通过@AuthenticationPrincipal
注解获取当前用户信息:@GetMapping("/user") public String user(@AuthenticationPrincipal OAuth2User user) { return "Hello, " + user.getAttribute("name"); }
1.2 使用 Keycloak
Keycloak 是一个开源的身份和访问管理解决方案,支持 OAuth2 和 OpenID Connect。
-
添加依赖:
<dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-spring-boot-starter</artifactId> </dependency>
-
配置 Keycloak:
在application.yml
中配置 Keycloak:keycloak: realm: <your-realm> auth-server-url: http://localhost:8080/auth resource: <your-client-id> public-client: true
-
配置 Spring Security:
在 Spring Security 配置类中启用 Keycloak:@KeycloakConfiguration public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); http .authorizeRequests() .anyRequest().authenticated(); } }
2. 基于 SAML 的单点登录
SAML(Security Assertion Markup Language)是一种基于 XML 的单点登录协议。
2.1 使用 Spring Security SAML
-
添加依赖:
<dependency> <groupId>org.springframework.security.extensions</groupId> <artifactId>spring-security-saml2-core</artifactId> <version>1.0.10.RELEASE</version> </dependency>
-
配置 SAML:
在application.yml
中配置 SAML:spring: security: saml2: relyingparty: registration: idp: entity-id: <idp-entity-id> sso-url: <idp-sso-url> verification: credentials: - certificate-location: "classpath:idp-certificate.crt" signing: credentials: - private-key-location: "classpath:private-key.pem" certificate-location: "classpath:certificate.crt"
-
配置 Spring Security:
在 Spring Security 配置类中启用 SAML 登录:@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .saml2Login(); } }
3. 基于 CAS 的单点登录
CAS(Central Authentication Service)是一种开源的单点登录协议。
3.1 使用 Spring Security CAS
-
添加依赖:
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-cas</artifactId> </dependency>
-
配置 CAS:
在application.yml
中配置 CAS:cas: server: url: https://cas.example.org/cas service: url: http://localhost:8080/login/cas
-
配置 Spring Security:
在 Spring Security 配置类中启用 CAS 登录:@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .cas() .loginProcessingUrl("/login/cas") .serviceProperties() .service("http://localhost:8080/login/cas"); } }
4. 基于 JWT 的单点登录
JWT(JSON Web Token)是一种轻量级的身份验证和授权机制。
4.1 使用 Spring Security JWT
-
添加依赖:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
-
生成和验证 JWT:
在服务端生成和验证 JWT:public String generateToken(String username) { return Jwts.builder() .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 3600000)) .signWith(SignatureAlgorithm.HS512, "secret") .compact(); } public String validateToken(String token) { return Jwts.parser() .setSigningKey("secret") .parseClaimsJws(token) .getBody() .getSubject(); }
-
配置 Spring Security:
在 Spring Security 配置类中启用 JWT 认证:@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .addFilter(new JwtAuthenticationFilter(authenticationManager())); } }
总结
Spring Boot 支持多种单点登录方案,常见的有:
-
OAuth2:适合与第三方身份提供商集成。
-
SAML:适合企业级单点登录。
-
CAS:适合集中式身份验证。
-
JWT:适合轻量级、无状态的认证。
----------------------------------------------------------------------------------
简介
Spring Security框架
描述
Spring Security是一个基于Spring框架的安全性框架,可以为Web应用程序提供身份验证(Authentication)、授权(Authorization)、攻击防御等安全功能。Spring Security框架提供了一整套的身份验证、授权、ACL(访问控制列表)等模块和类库,还提供了一系列的安全过滤器、安全标签等,可以方便地实现常见的安全性控制。
RBAC 权限模型
基于角色的权限控制
RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等;
基于资源的权限控制
RBAC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,比如:用户必须具有查询工资权限才可以查询员工工资信息等;
两种方式的优缺点对比
基于角色的权限控制
优点: 容易配置,只需要把每个角色的权限在配置好,保存在数据库,所有用户在注册的时候就分配好角色,那么所有的用户的权限可以同一配置了,当角色的权限发生变化的时候,只需要将对应的角色的权限配置好就行了。
缺点: 有些用户归属于同一角色,但是因为岗位略有不同,那么需要一些特殊的权限,那么需要子角色,如果一直这样下去,可能有子角色的子角色…可能需要一直创建很多子级角色。概括来说,粒度不够细。
注: 角色相当于用户和权限的之间的一个中间商;
基于资源的权限控制
优点: 权限的控制的粒度比较灵活,可以按照不同资源模块来限制用户的权限,比如客户端用户不能访问系统管理,那么不用管系统管理里面有多少功能API,不用细分地给用户分配每一个API的权限,一律屏蔽不允许,也可以允许访某个资源模块中的限定的一些API;
缺点: 需要批量修改用户的权限的时候就很麻烦。
综合对比还是基于角色的权限控制实现会更便于控制,虽然同一角色会特殊情况会有不同的功能权限,但是只需要创建子角色就可以解决,这样,即使需要改变某些群体的权限就可以修改角色的权限达到批量修改的目的,但是如果纯粹按照基于资源的权限控制方式设计权限控制,那么当有些用户权限需要变动的时候,不能批量修改。
Spring Security的核心组件
名称 描述
Authentication 身份验证组件,负责用户身份认证
Authorization 权限授权组件,负责用户权限的授权管理
Access Control、ACL 访问控制列表组件,负责资源的访问控制和权限的分配控制
Session Management 会话管理组件,负责管理用户的会话,如Session ID管理等
Web Security 使用Spring Security保护Web应用程序的安全组件
Remember-me 记住我功能组件,负责实现自动登录功能
OpenID OpenID功能组件,负责与Open ID提供商的集成
Spring Security的主要特点包含:
可扩展性: Spring Security框架提供了很多可扩展的类和接口,可以通过自定义实现这些接口和类来满足自己的需求。
配置简单: Spring Security框架的配置非常简单,只需配置几个关键的类即可实现基本的安全性控制。
弹性设计: Spring Security框架非常灵活,可以根据实际情况在不同的场景下灵活应对。
多种安全认证方式: Spring Security框架提供了很多种安全认证方式,如表单认证、基本认证、OAuth2等,可以根据实际需求选择合适的认证方式。
大体认证授权流程
官方说明:
Spring Security的认证机制包括AuthenticationManager和AuthenticationProvider两个核心组件。
AuthenticationManager:是一个接口,定义了身份验证的入口方法authenticate(),该方法接受一个Authentication对象作为参数,并返回一个封装了认证信息的Authentication对象。
AuthenticationProvider:是一个接口,定义了身份验证的具体实现,该接口的实现类可以根据不同的身份验证方式(如用户名密码、数字证书等)来实现身份验证的功能。
在Spring Security中,用户请求经过过滤器链,经过身份认证和授权决策来保护资源的安全。
身份认证包括认证请求的处理和身份验证的实现。
认证请求的处理: 包括UsernamePasswordAuthenticationFilter、RememberMeAuthenticationFilter等过滤器的处理。
身份验证的实现: 则是通过AuthenticationManager来实现的。
授权决策由AccessDecisionManager和AccessDecisionVoter两个核心组件来实现。
AccessDecisionManager: 是一个接口,定义了授权决策的入口方法decide(),该方法接受三个参数:Authentication对象(当前用户的认证信息)、Object对象(正在访问的资源)、List对象(访问资源所需的权限列表)。
AccessDecisionVoter: 是一个接口,定义了对当前用户的认证信息、当前请求所需的权限、资源的访问控制列表进行比较的方法,以决定当前用户是否有访问该资源的权限。
注: Spring Security的认证授权原理: 是通过 AuthenticationManager和AuthenticationProvider实现身份认证,通过AccessDecisionManager和AccessDecisionVoter实现授权决策,以保护资源的安全。
具体的执行流程其实是一个过滤链:
1、用户向应用程序发起请求,请求需要经过Spring Security的过滤器链。
2、过滤器链首先会经过UsernamePasswordAuthenticationFilter过滤器,该过滤器判断请求是否是一个认证请求。如果是认证请求,过滤器将获取请求中的用户名和密码,然后使用AuthenticationManager进行身份认证。
3、AuthenticationManager会根据用户名和密码创建一个Authentication对象,并将该对象传递给AuthenticationProvider进行认证。
4、AuthenticationProvider会根据传递过来的Authentication对象进行身份认证,并返回一个认证成功或失败的结果。
5、如果认证成功,UsernamePasswordAuthenticationFilter会将认证信息封装成一个Authentication对象,并将其放入SecurityContextHolder上下文中。
6、用户请求获取资源时,会经过FilterSecurityInterceptor过滤器,该过滤器会根据请求的URL和HTTP方法获取访问控制列表(Access
Control List)。
7、Access Control List会包含访问资源所需要的权限信息,FilterSecurityInterceptor会将Authentication对象和Access
Control List传递给AccessDecisionManager进行授权决策。
8、AccessDecisionManager会调用多个AccessDecisionVoter进行投票,并根据投票结果来决定当前用户是否有访问该资源的权限。如果用户被授权访问资源,应用程序将返回资源的响应结果。
总结就是首先经过认证过滤器实现认证,认证成功的话就会将用户信息存到authentication对象里面放到security上下文去(后续的权限校验需要获取到),这里面是包括权限的,之后再由AccessDecisionManager去根据相关策略进行权限鉴定
JWT框架
描述
JWT全称叫json web token,通过数字签名的⽅式,以json对象为载体,在不同的服务终端之间安全的传输信息。
1、JWT在前后端分离系统,或跨平台系统中,通过JSON形式作为WEB应⽤中的令牌,⽤于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中,还可以完成数据加密、签名等相关处理。
2、前端应⽤在访问后端应⽤时,会携带令牌,后端应⽤在接到令牌后,会验证令牌的合法性。从⽽决定前端应⽤是否能继续访问。
3、JWT还可以系统之间进⾏信息传递,A系统通过令牌对B系统进⾏数据传输,在传输过程中,可以完成数据的加密,B系统拿到数据后,通过签名进⾏验证,从⽽判断信息是否有篡改。
4、JWT就是一个签名验证的框架,负责将用户信息进行编码加密,编码加密后再将加密后的数据返回给浏览器。浏览器下一次请求只需要带上这个令牌即可。服务器端就可以根据这个令牌来验证用户的身份信息。
5、JWT是一种客户端浏览器和服务器之间传递安全信息的一种声明规范
应用
JWT最常⻅的场景就是授权认证,⼀旦⽤户登录,后续每个请求都将包含JWT,系统在每次处理⽤户请求之前,都要先进⾏JWT安全校验,通过之后再进⾏处理。
授权
这是JWT最常⻅⽅案,⼀旦⽤户登录,每个后续请求将包括JWT,从⽽允许⽤户访问该令牌允许的路由,服务和资源。
信息交换
JWT是在各⽅之间安全地传输信息的好⽅法,可以验证传输内容是否遭到篡改。
JWT和session的区别:
1、jwt可以隐藏数据、安全系数更高(安全性更好)
2、jwt更适合分布式/微服务系统
3、session是将数据存储在应用服务器里的,当用户量较大的时候,会造成服务资源的浪费。
4、session是存在单台服务器上的,如果涉及到分布式应用,那么因为每一台服务器上存储的session不一致导致用户状态丢失等问题。
5、jwt所产生的验证字符串(token)是不会存储在服务器的,而是存储到客户端。
专业术语
1、每个⽤户经过我们的应⽤认证之后,将认证信息保存在session中,由于session服务器中对象,随着认证⽤户的增多,服务器内存开销会明显增⼤。
2、⽤户认证之后,服务端使⽤session保存认证信息,那么要取到认证信息,只能访问同⼀台服务器,才能拿到授权的资源。这样在分布式应⽤上,就需要实现session共享机制,不⽅便集群应⽤。
3,JWT认证基于令牌,该令牌存储在客户端。但认证由服务器端进⾏,解决服务器内存占⽤问题。当⽤户提交⽤户名和密码时,在服务器端认证通过后,会⽣成token令牌。然后将令牌响应给客户端浏览器。
4,客户端浏览器会在本地存储令牌。在客户端再次请求服务器接⼝时,每次都会携带JWT,在服务器验证通过后,再继续访问服务器资源。
优势
1、简洁明了,可以通过URL、POST参数或Http header发送,因为数据量⼩,传输速度快。用户只需要关心密钥安全性问题。
2、⾃包含,jwt的负载可以传递一些⽤户基本的信息,不需要在服务器端保存会话信息(不需要再将数据保存在服务器里),不占服务器内存,也避免了多次查询数据库,特别适⽤于分布式微服务。
3、因为(jwt)token是以json加密的形式保存在客户端的,所以JWT是可以跨语⾔使⽤(如:python、go),原则上任何WEB形式都⽀持。很多编程语言都可以使用jwt进行授权。
JWT认证流程
1、首先由客户端浏览器发送登录请求,服务器收到登录请求后进行用户身份的验证。(账户、密码的验证),验证失败,将失败信息回执给浏览器(用户名/密码错误),验证通过,会进行jwt加密授权。
2、JWT可以将用户信息存放在其内部结构里(负载中(payload)),将header(头部)和负载进行Base64编号拼接后进行签名(加密)签名时需要提供自定的一串密钥(密钥是jwt里重要的一环,密钥决定用户数据是否能够被篡改)。最后返回一个token令牌(形成JWT)。
3、后端服务器再将令牌(JWT字符串)作为登录成功的返回结果发送给客户端浏览器,客户端浏览器将返回结果存在localStorage对象里。
4、客户端浏览器下一次请求只需要将令牌(JWT)放在在header里的的Authorization位。
5、后端检查是否存在,如果验证令牌(JWT)有效,后端就可以使⽤JWT中包含的⽤户信息。
JWT组成结构
jwt是由三个部分组成,JWT其实就是⼀段字符串,由3部分组成,⽤ . (点)拼接
JWT字符串示例
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2M
jc5NzA5NDUsInVzZXIiOiJ7XCJpZFwiOjMsXCJuYW1lXCI6XCL
lkInlkIlcIixcInB3ZFwiOlwiMzMzXCJ9In0.NvGdUjFLJWj_
ZzhY9Qp--NkZgK1QGQtQjiCB7lEsFTg
1
2
3
4
标头:header——
一般不需要手动改设置,JWT会自动进行设置。
头部主要分为两部分:
typ: “JWT” 加密类型
alg: "hs256"签名算法。
Header{ //标头
‘typ’:’JWT’, 表示token类型
‘alg’:’HS256’ 表示签名算法
}
1
2
3
4
负载:payload——
负载一般设置的是用户基本数据。⽤于存储用户的主要信息,使⽤ Base64编码组成JWT结构的第⼆部分。由于该信息是可以被解析的,所以不要存放敏感信息在第二部分
{name:"用户名",phoneNumber:"13878654547"};
1
Payload //有效负载
{
"userCode":"43435",
"name":"john",
"phone’:"13950497865"
}
1
2
3
4
5
签名:signature——
将编码过后的表头header和负载payload这两部分数据进行加密(加密需要使用到一个自定义的密钥。)
注: 前两部分都使⽤Base64进⾏编码,前端可以解开知道⾥⾯的信息, Signature需要使⽤编码后的header和payload以及我们提供的⼀密钥,然后使⽤header中指定的签名算法进⾏签名,以保证JWT没有被篡改过。
使⽤Signature签名可以防⽌内容被篡改。如果有⼈对头部及负载内容解码后进⾏修改,再进⾏编码,最后加上之前签名组成新的JWT。那么服务器会判断出新的头部和负载形成的签名和JWT附带的签名是不⼀样的。如果要对新的头部和负载进⾏签名,在不知道服务器加密时⽤的密钥的话,得出来的签名也是不⼀样的
OAuth2框架
描述
OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。
OAuth相关的名词
Third-party application 第三方应用程序,比如这里的虎牙直播;
HTTP service HTTP服务提供商,比如这里的QQ(腾讯);
Resource Owner 资源所有者,就是QQ的所有人,你;
User Agent 用户代理,这里指浏览器;
Authorization server 认证服务器,这里指QQ提供的第三方登录服务;
Resource server 资源服务器,这里指虎牙直播提供的服务,比如高清直播,弹幕发送等(需要认证后才能使用)。
服务器
OAuth2主要包含认证服务器和资源服务器这两大块的实现。
认证服务器(也称:授权服务器)
主要包含了四种授权模式的实现和Token的生成与存储
资源服务器
主要是在Spring Security的过滤器链上加了OAuth2AuthenticationProcessingFilter过滤器,即使用OAuth2协议发放令牌认证的方式来保护我们的资源
四种授权模式
客户端凭证模式
客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)确认客户端身份无误后,将令牌(access_token)发送给client,请求如下:
url?client_id=c1&client_secret=secret&grant_type=client_credentials
1
参数列表如下:
client_id: 客户端准入标识。
client_secret: 客户端秘钥。
grant_type: 授权类型,填写client_credentials表示客户端模式
优缺点: 这种模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任,而client本身也是安全的。因此这种模式一般用来提供给我们完全信任的服务器端服务。比如,合作方系统对接,拉取一组用户信息。
总结来说: 该模式针对客户端而言,对用户是透明的,不需要用户参与,非用户层面授权。客户端向授权服务器发送自己的的 client_id 和client_secrect 请求 access_token ,用户中心仅校验客户端应用身份。客户端通过授权后可以获得授权范围内所有用户的信息,对客户端应用需要极高的信任。
模式特点: 针对客户端层面进行授权,而非对单独用户进行授权的场景,用户中心仅校验客户端应用的身份。
适用场景: 适用于的自家产品、微服务中,需要从接口层面发起请求的场景。
注: 当前模式仅生成 access_token,不生成 refresh_token。
密码模式
资源拥有者将用户名、密码发送给客户端,客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token),请求如下:
授权服务器将令牌(access_token)发送给client
url?client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123
1
参数列表如下:
client_id: 客户端准入标识。
client_secret: 客户端秘钥。
grant_type: 授权类型,填写password表示密码模式
username: 资源拥有者用户名(账号)。
password: 资源拥有者密码(密码)。
授权服务器将令牌(access_token)发送给client
这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client,把用户名和密码直接泄露给客户端,代表了资源拥有者和授权服务器对客户端的绝对互信,相信客户端不会做坏事。一般适用于内部开发的客户端的场景。因此这就说明这种模式只能用于client是我们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用。
总结来说: 用户直接提供用户名与密码给客户端应用,客户端使用用户的账号和密码、自己的 client_id 和 client_secrect 向授权服务器请求 token 。用户中心校验用户和客户端应用身份,响应 access_token 和 refresh_token。
模式特点: 用户的账号和密码直接暴露给客户端,安全性低。
适用场景: 适用于自家的产品、微服务中,需要从用户层面发起请求的场景。
注: 该模式已经被 Oauth2.1 废弃,直接将用户的账号和密码明文交给客户端应用是一个传统的方案,其本身没有校验意义,需要逐步过渡到通过 token 凭证这种授权方式。
换而言之,客户端都有密码了,通过 Oauth2.0 流程要一个临时的 access_token 干嘛?
授权码模式
资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息。如:
url?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
1
参数列表如下:
client_id: 客户端准入标识。
response_type: 授权码模式固定为code。
scope: 客户端权限。
redirect_uri: 跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。
1、浏览器出现向授权服务器授权页面,之后将用户同意授权。
2、授权服务器将授权码(AuthorizationCode)转经浏览器发送给client(通过redirect_uri)。
3、客户端拿着授权码向授权服务器索要访问access_token,请求如下:
url? client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://w ww.baidu.com
1
参数列表如下
client_id: 客户端准入标识。
client_secret: 客户端秘钥。
grant_type: 授权类型,填写authorization_code,表示授权码模式
code: 授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri: 申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
总结来说: 客户端应用引导(重定向)用户携带着 client_id 和 redirect_url 前往认证服务器认证,认证通过后认证服务会附带上 code 参数重定向到redirect_url 地址(客户端应用提供的接收授权码的地址)。客户端应用的服务端携带自己的 client_id 、 client_secrect 和 code 请求认证服务器获取 access_token 和 refresh_token 返回到用户。
模式特点: 四种模式中最安全、最常见的一种模式。
适用场景: 适用于有服务端服务的客户端应用。
网上说,这种方式避免了 access_token 直接在公网传输,黑客截获到 code 也无法获得最终的 access_token,所以这种方案非常安全。不是非常认同,如果 code 会被截获,那么用户登录时的 token 是否也可以直接被截获到?
个人观点认为,安全主要体现在这种模式同时校验了用户和客户端应用的 client_secrect ,与密码模式不同,当前模式用户和客户端应用双方都不知道对方的密码明文。
隐式授权(简易模式)
资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器重定向到授权服务器,重定向时会附加客户端的身份信息。如:
url?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
1
参数描述同授权码模式 ,注意response_type=token,说明是简化模式。
浏览器出现向授权服务器授权页面,之后需要用户同意授权。
授权服务器将授权码将令牌(access_token)以Hash的形式存放在重定向uri的fargment中发送给浏览器。
注: fragment 主要是用来标识 URI 所标识资源里的某个资源,在 URI 的末尾通过(#)作为 fragment 的开头,
其中 # 不属于 fragment 的值。如https://domain/index#L18这个 URI 中 L18 就是 fragment 的值。大家只需要
知道js通过响应浏览器地址栏变化的方式能获取到fragment 就行了。
一般来说,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码。
浏览器访问认证页面:
url? client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
1
然后输入模拟的账号和密码点登陆之后进入授权页面:
确认授权后,浏览器会重定向到指定路径(oauth_client_details表中的web_server_redirect_uri)并以Hash的形式存放在重定向uri的fargment中,如:
http://aa.bb.cc/receive#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZW5hbn...
1
总结来说: 客户端应用引导用户携带 client_id 前往授权服务器认证,认证通过后认证服务器直接返回access_token。
模式特点: 不需要与客户端应用的服务端进行交互,没有校验 client_secrect。与授权码模式,省去返回授权码,再重定向到授权服务,直接一步到位。
适用场景: 适用于仅有前端页面,没有后端服务的客户端应用。
通过 # 锚点链接的方式返回 access_token,避免 token 被携带传输到 web 前端文件托管的服务器上。
注: 该模式已经被 Oauth2.1 废弃,这种方式容易泄露 token ,不安全。但是token以#fragment 返回,并传输,容易泄露
-----------------------------------------------------------------------------------
单点登录是目前比较流行的企业业务整合的解决方案之一。单点登录是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。例如:百度旗下有很多的产品,比如百度贴吧、百度知道、百度文库等,只要登录百度账号,在任何一个地方都是已登录状态,不需要重新登录。
单点登录是互联网应用和企业级平台中的基础组件服务。接下来就介绍单点登录的原理,并基于SpringBoot +JWT实现单点登录解决方案。
一、什么是单点登录?
单点登录(Single Sign On 简称 SSO)是一种统一认证和授权机制,指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统,不再需要重新登录验证。
单点登录一般是用于互相授信的系统,实现单一位置登录,其他信任的应用直接免登录的方式。在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。
单点登录是互联网应用和企业级平台中的基础组件服务。比如百度贴吧、百度知道、百度文库等,只要登录百度账号,在任何一个地方都是已登录状态,不需要重新登录。此外,第三方授权登录,如在京东中使用微信登录。解决信息孤岛和用户不对等的实现方案。
随着时代的演进,大型web系统早已从单体应用架构发展为如今的多系统分布式应用群。但无论系统内部多么复杂,对用户而言,都是一个统一的整体,访问web系统的整个应用群要和访问单个系统一样,登录/注销只要一次就够了,不可能让一个用户在每个业务系统上都进行一次登录验证操作,这时就需要独立出一个单独的认证系统,它就是单点登录系统。
二、单点登录的优点和不足
单点登录解决了用户只需要登录一次就可以访问所有相互信任的应用系统,而不用重复登录。除此之外,还有以下优点:
1)提高用户的效率。用户不再被多次登录困扰,也不需要记住多个 ID 和密码。另外,用户忘记密码并求助于支持人员的情况也会减少。
2)提高开发人员的效率。SSO 为开发人员提供了一个通用的身份验证框架。实际上,如果 SSO 机制是独立的,那么开发人员就完全不需要为身份验证操心。他们可以假设,只要对应用程序的请求附带一个用户名,身份验证就已经完成了。
3)简化管理。如果应用程序加入了单点登录协议,管理用户账户的负担就会减轻。简化的程度取决于应用程序,因为 SSO 只处理身份验证。所以,应用程序可能仍然需要设置用户的属性(比如访问特权)。
三、单点登录实现机制
单点登录听起来很复杂,实际上架构非常简单,具体如下图所示:

如上图所示,当用户第一次访问应用系统A时,因为还没有登录,会被跳转到认证系统进行登录;认证系统根据用户提供的登录信息进行身份校验,如果通过,则返回给用户一个认证凭据(ticket);用户访问其他应用系统时,将会带上此ticket作为自己认证的凭据,应用系统B接受到请求之后会把ticket送到认证系统进行校验,检查ticket的合法性。如果通过,则成功登录应用系统B。
四、单点登录常见的解决方案
实现单点登录的方式有很多种,常见的有基于Cookie、Session共享、Token机制、JWT机制等方式。
4.1 基于Cookie
基于Cookie是最简单的单点登录实现方式是使用cookie作为媒介存放用户凭证。用户登录成功后,返回一个加密的cookie,当用户访问其他应用时,携带此cookie,授权应用解密cookie并进行校验,校验通过则登录当前用户。

当然,我们不难发现以上方式将信任数据存储在客户端的Cookie中,这种方式存在两个问题:
1、Cookie不安全,CSRF(跨站请求伪造);
2、Cookie不能跨域,无法解决跨域系统的问题。
对于第一个问题,通过加密Cookie可以保证安全性,当然这是在源代码不泄露的前提下。如果Cookie的加密算法泄露,攻击者通过伪造Cookie则可以伪造特定用户身份,这是很危险的。
对于第二个问题,不能跨域是Cookie的硬伤。可以将生成ticket作为参数传递到其他应用系统。这样可以避免跨域问题。
4.2 基于Session共享
所谓基于Session共享,主要是将Session会话信息保存到公共的平台,如Redis,数据库等,各应用系统共用一个会话状态sessionId,实现登录信息的共享,从而实现单点登录。
基于Session解决了Cookie不能跨域的问题,但也存在其他问题。早期的单体应用使用Session实现单点登录,但现在大部分情况下都需要集群,由于存在多台服务器,Session在多台服务器之间是不共享的,因此,还需解决Session共享的问题
解决系统之间的 Session 不共享问题有以下几种方案:
1)Tomcat集群Session全局复制【会影响集群的性能呢,不建议】
2)分布式Session,即把Session数据放在Redis中(使用Redis模拟Session)【建议】
4.3 Token机制
其实,Token就是一串加密(使用MD5,等不可逆加密算法)的字符串。具体流程如下:
1.客户端使用用户名跟密码请求登录,
2.服务端收到请求,去验证用户名与密码,
3.验证成功后,服务端会签发一个加密的字符串(Token)保存到(Session,Redis,Mysql)中,并把这个Token发送给客户端,
4.客户端收到Token后存储在本地,如:Cookie 或 Local Storage 中,
5.客户端每次向服务端请求资源的时候需要带着服务端签发的 Token,
6.服务端收到请求,验证客户端请求中携带Token和服务器中保存的Token进行对比效验, 如果验证成功,就向客户端返回请求的数据。
使用Token验证的优势:
- 无状态、可扩展;
- 在客户端存储的Token是无状态的,并且可扩展。基于这种无状态和不存储Session信息,负载负载均衡器能够将用户信息从一个服务传到其他服务器上;
- 安全性,请求资源时发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)
即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。
4.4 JWT 机制
JWT(JSON Web Token的缩写)它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证JWTToken的正确性,只要正确即通过验证。
4.4.1 JWT的特点
- 紧凑:数据小,可以通过URL,POST参数,请求头发送。且数据小代表传输速度快。
- 自包含:使用payload数据块记录用户必要且不隐私的数据,可以有效减少数据库访问次数,提高代码性能。
- JWT一般用于处理用户身份验证或数据信息交换。
- 用户身份验证:一旦用户登录,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小,并且能够轻松地跨不同域使用。
- 数据信息交换:JWT是一种非常方便的多方传递数据的载体,因为其可以使用数据之前确保数据的有效性和安全性。
4.4.2 JWT数据结构
JWT的结构包含三个部分:Header头部,Payload负载和Signature签名。三部分之间用“.”号做分割。校验也是JWT内部自己实现的 ,并且可以将你存储时候的信息从token中取出来无须查库。
- header
数据结构:{“alg”: “加密算法名称”, “typ” : “JWT”}
alg是加密算法定义内容,如:HMAC SHA256 或 RSA
typ是token类型,这里固定为JWT。
- payload
在payload数据块中一般用于记录实体(通常为用户信息)或其他数据的。主要分为三个部分,分别是:已注册信息(registered claims),公开数据(public claims),私有数据(private claims)。
payload中常用信息有:iss(发行者),exp(到期时间),sub(主题),aud(受众)等。前面列举的都是已注册信息。
公开数据部分一般都会在JWT注册表中增加定义。避免和已注册信息冲突。
公开数据和私有数据可以由程序员任意定义。
注意:即使JWT有签名加密机制,但是payload内容都是明文记录,除非记录的是加密数据,否则不排除泄露隐私数据的可能。不推荐在payload中记录任何敏感数据。
- Signature
签名信息。这是一个由开发者提供的信息。是服务器验证的传递的数据是否有效安全的标准。在生成JWT最终数据前,先使用header中定义的加密算法,将header和payload进行加密,并使用点进行连接。如:加密后的head.加密后的payload。再使用相同的加密算法,对加密后的数据和签名信息进行加密。得到最终结果。
4.4.3 JWT执行流程
JWT的请求流程也特别简单,首先使用账号登录获取Token,然后后面的各种请求,都带上这个Token即可。具体流程如下:
1. 客户端发起登录请求,传入账号密码;
2. 服务端使用私钥创建一个Token;
3. 服务器返回Token给客户端;
4. 客户端向服务端发送请求,在请求头中携带Token;
5. 服务器验证该Token;
6. 返回结果。

可能有些小伙伴会觉得,Token 和JWT有什么区别呢?其实Token和JWT确实比较类似,只不过,Token需要查库验证token 是否有效,而JWT不用查库,直接在服务端进行校验,因为用户的信息及加密信息,和过期时间,都在JWT里,只要在服务端进行校验就行,并且校验也是JWT自己实现的。
五、基于JWT机制的单点登录
JWT提供了基于Java组件:java-jwt帮助我们在Spring Boot项目中快速集成JWT,接下来进行SpringBoot和JWT的集成。接下来我们通过项目示例,演示如何基于SpringBoot+JWT实现单点登录。
5.1 项目结构
项目结构如下图所示:

如上图所示,weiz-sso为认证系统,weiz-app-a和weiz-app-b为两个独立的应用系统。
5.2 创建认证系统
5.2.1.创建项目并引入JWT等依赖
首先,创建普通的Spring Boot项目weiz-sso,修改项目中的pom.xml文件,引入JWT等依赖。示例代码如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- SpringBoot集成thymeleaf模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
5.2.2.创建&验证JWT工具类
创建通用的处理类TokenUtil,负责创建和验证Token。示例代码如下:
/**
* 类功能简述:
* 类功能详述:
*
* @author weiz
*/
public class JwtUtil {
/**
* Description: 生成一个jwt字符串
*
* @param name 用户名
* @param secret 秘钥
* @param timeOut 超时时间(单位s)
* @return java.lang.String
* @author weiz
*/
public static String encode(String name, String secret, long timeOut) {
Algorithm algorithm = Algorithm.HMAC256(secret);
String token = JWT.create()
//设置过期时间为一个小时
.withExpiresAt(new Date(System.currentTimeMillis() + timeOut))
//设置负载
.withClaim("name", name)
.sign(algorithm);
return token;
}
/**
* Description: 解密jwt
*
* @param token token
* @param secret secret
* @return java.util.Map<java.lang.String , com.auth0.jwt.interfaces.Claim>
* @author weiz
*/
public static Map<String, Claim> decode(String token, String secret) {
if (token == null || token.length() == 0) {
throw new CustomException("token为空:" + token);
}
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return decodedJWT.getClaims();
}
}
5.2.3 登录功能
接下来创建AuthController控制器,实现登录,退出等请求接口,示例代码如下:
/**
* 类功能简述:
* 类功能详述:
*
* @author weiz
*/
@Controller
@RequestMapping("/sso")
public class AuthController {
private JwtService service;
@Autowired
public AuthController(JwtService service) {
this.service = service;
}
@RequestMapping({"/","/index"})
public String index(){
return "index";
}
@RequestMapping("/login")
public String login(){
return "login";
}
@RequestMapping(value = "/login",method = RequestMethod.POST)
@ResponseBody
public ReturnResult login(@RequestBody User user) {
String token = service.login(user);
return ReturnResult.successResult(token);
}
@RequestMapping("/checkJwt")
@ResponseBody
public ReturnResult checkJwt(String token) {
return ReturnResult.successResult(service.checkJwt(token));
}
@RequestMapping("/refreshJwt")
@ResponseBody
public ReturnResult refreshJwt(String token){
return ReturnResult.successResult(service.refreshJwt(token));
}
@RequestMapping("/inValid")
@ResponseBody
public ReturnResult inValid(String token) {
service.inValid(token);
return ReturnResult.successResult(null);
}
}
5.3 创建应用系统
5.3.1创建应用系统weiz-app-a和weiz-app-b
首先,分别创建两个Spring Boot项目weiz-app-a和weiz-app-b。并引入相关依赖,示例代码如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- SpringBoot集成thymeleaf模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.14.9</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
5.3.2 登录验证拦截器
在两个项目中分别添加登录拦截器LoginFilter,负责拦截所有Http请求,验证Token是否有效。示例代码如下:
/**
* 类功能简述:
* 类功能详述:
*
* @author weiz
*/
@Component
@WebFilter(urlPatterns = "/**", filterName = "loginFilter")
public class LoginFilter implements Filter {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Value("${sso_server}")
private String serverHost;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String token = httpServletRequest.getParameter("token");
if (this.check(token)) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
HttpServletResponse response = (HttpServletResponse) servletResponse;
String redirect = serverHost + "/login?redirect=" + httpServletRequest.getRequestURL();
//response.setContentType("application/json;charset=utf-8");
//response.setCharacterEncoding("utf-8");
//response.getWriter().write(JSON.toJSONString(new ReturnEntity(-1, "未登录", null)));
response.sendRedirect(redirect);
}
}
private boolean check(String jwt) {
try {
if (jwt == null || jwt.trim().length() == 0) {
return false;
}
JSONObject object = HttpClient.get(serverHost + "/checkJwt?token=" + jwt);
return object.getBoolean("data");
} catch (Exception e) {
logger.error("向认证中心请求失败", e);
return false;
}
}
}
5.3.3.创建控制器
在两个项目中分别创建IndexController控制器,处理HTTP请求。示例代码如下:
/**
* 类功能简述:
* 类功能详述:
*
* @author weiz
*/
@Controller
public class IndexController {
@Value("${sso_server}")
private String serverHost;
@RequestMapping({"/","/index"})
public String index() {
return "index";
}
@RequestMapping("/test")
@ResponseBody
public ReturnEntity test() {
return new ReturnEntity(1, "通过验证", null);
}
@RequestMapping("/logout")
public void logout(HttpServletRequest request, HttpServletResponse response,String token) throws Exception {
HttpClient.get(serverHost + "/inValid?token=" + token);
String requestHost = request.getScheme() +"://"+ request.getServerName() + ":"+request.getServerPort() +"/";
String redirect = serverHost + "/login?redirect=" + requestHost;
System.out.println(redirect);
response.sendRedirect(redirect);
}
}
5.4 测试验证
集成JWT成功之后,接下来验证单点登录系统是否成功,分别启动wei-sso、weiz-app-a和weiz-app-b。验证功能是否正常。
首先,在浏览器中访问应用系统A:http://localhost:8081/

如上图所示,由于没有登录,自动跳转到了单点登录系统进行登录验证。输入用户名、密码(admin\admin)登录成功并返回到应用系统A。

接下来,使用此token 访问应用系统B,在浏览器中输入:http://localhost:8082/?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYWRtaW4iLCJleHAiOjE2NTc4MDA4MTB9.7NLXFJwGKxgnEwBsE25OredrpLIaanAoqeHXSZjO6QA

如上图所示,通过之前的token,无需登录即可成功进入了应用系统B。说明我们的单点登录系统搭建成功。
总结
以上,我们就把单点登录(SSO)的所有流程都介绍完了,原理大家都清楚了。单点登录是互联网应用和企业级平台中的基础组件服务,希望大家能明白其中的原理,熟练掌握。