[TOC]
1 Oauth2 介绍
1.1 什么是 OAuth 2
- OAuth 是一个开放标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如头像、照片、视频等),而在这个过程中无须将用户名和密码提供给第三方应用。实现这一功能是通过提供一个令牌(token),而不是用户名和密码来访问他们存放在特定服务提供者的数据。
- 每一个令牌授权一个特定的网站在特定的时段内访问特定的资源。这样,OAuth 让用户可以授权第三方网站灵活地访问存储在另外一些资源服务器的特定信息,而非所有内容。目前主流的 qq,微信等第三方授权登录方式都是基于 OAuth2 实现的。
- OAuth 2 是 OAuth 协议的下一版本,但不向下兼容 OAuth 1.0。OAuth 2 关注客户端开发者的简易性,同时为 Web 应用、桌面应用、移动设备、起居室设备提供专门的认证流程。
- 传统的 Web 开发登录认证一般都是基于 Session 的,但是在前后端分离的架构中继续使用 Session 会有许多不便,因为移动端(Android、iOS、微信小程序等)要么不支持Cookie(微信小程序),要么使用非常不便,对于这些问题,使用 OAuth 2 认证都能解决。
1.2 OAuth 2 授权流程
如图11-1所示,授权步骤有下面6歩
- 步骤1:客户端(第三方应用)向用户请求授权。
- 步骤2:用户单击客户端所呈现的服务授权页面上的同意授权按钮后,服务端返回一个授权许可凭证给客户端。
- 步骤3:客户端拿着授权许可凭证去授权服务器申请令牌。
- 步骤4:授权服务器验证信息无误后,发放令牌给客户端。
- 步骤5:客户端拿着令牌去资源服务器访问资源。
- 步骤6:资源服务器验证令牌无误后开放资源。
1.3 OAuth 2 角色
资源所有者(Resource Owner):即代表授权客户端访问本身资源信息的用户,客户端访问用户帐户的权限仅限于用户授权的“范围”。
客户端(Client):即代表意图访问受限资源的第三方应用。在访问实现之前,它必须先经过用户者授权,并且获得的授权凭证将进一步由授权服务器进行验证。
授权服务器(Authorization Server):授权服务器用来验证用户提供的信息是否正确,并返回一个令牌给第三方应用。
资源服务器(Resource Server):资源服务器是提供给用户资源的服务器,例如头像、照片、视频等。
1.4 OAuth 2 授权模式
OAuth 协议的授权模式共分为 4 种,分别说明如下:
- 授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本 都是使用这种模式。
- 简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器中请令牌,一般若网站是纯静态页面,则可以采用这种方式。
- 密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器中请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。
- 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。
2 Spring Security Oauth2.0 入门案例
2.1 授权服务器
工程结构如下图:
2.1.1 搭建授权工程
pom.xml
<?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.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.lxs</groupId>
<artifactId>auth-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>auth-server</name>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR5</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>
启动器配置文件
com\lxs\oauth2\AuthServerApplication.java
package com.lxs.oauth2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
@SpringBootApplication
@EnableResourceServer
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
}
}
oauth2-demo\auth-server\src\main\resources\application.yml
server:
port:
8888
2.1.2 配置类
Oauth2配置类
完整代码
com\lxs\oauth2\config\OAuth2Config.java
package com.lxs.oauth2.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
@Configuration
//开启授权服务
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
private static final String CLIENT_ID = "cms";
private static final String SECRET_CHAR_SEQUENCE = "{noop}secret";
private static final String SCOPE_READ = "read";
private static final String SCOPE_WRITE = "write";
private static final String TRUST = "trust";
private static final String USER ="user";
private static final String ALL = "all";
private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 30*60;
private static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 30*60;
// 密码模式授权模式
private static final String GRANT_TYPE_PASSWORD = "password";
//授权码模式
private static final String AUTHORIZATION_CODE = "authorization_code";
//refresh token模式
private static final String REFRESH_TOKEN = "refresh_token";
//简化授权模式
private static final String IMPLICIT = "implicit";
//指定哪些资源是需要授权验证的
private static final String RESOURCE_ID = "resource_id";
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
// 使用内存存储
.inMemory()
//标记客户端id
.withClient(CLIENT_ID)
//客户端安全码
.secret(SECRET_CHAR_SEQUENCE)
//为true 直接自动授权成功返回code
.autoApprove(true)
.redirectUris("http://127.0.0.1:8084/cms/login") //重定向uri
//允许授权范围
.scopes(ALL)
//token 时间秒
.accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
//刷新token 时间 秒
.refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS)
//允许授权类型
.authorizedGrantTypes(GRANT_TYPE_PASSWORD,AUTHORIZATION_CODE);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 使用内存保存生成的token
endpoints.authenticationManager(authenticationManager).tokenStore(memoryTokenStore());
}
/**
* 认证服务器的安全配置
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
//.realm(RESOURCE_ID)
// 开启/oauth/token_key验证端口认证权限访问
.tokenKeyAccess("isAuthenticated()")
// 开启/oauth/check_token验证端口认证权限访问
// .checkTokenAccess("isAuthenticated()")
.checkTokenAccess("permitAll()")
//允许表单认证
.allowFormAuthenticationForClients();
}
@Bean
public TokenStore memoryTokenStore() {
// 最基本的InMemoryTokenStore生成token
return new InMemoryTokenStore();
}
}
Spring Security配置类
完整代码
com\lxs\oauth2\config\SecurityConfig.java
package com.lxs.oauth2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception { //auth.inMemoryAuthentication()
auth.inMemoryAuthentication()
.withUser("lxs")
.password("{noop}123")
.roles("admin");
}
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/asserts/**");
web.ignoring().antMatchers("/favicon.ico");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http // 配置登录页并允许访问
.formLogin().permitAll()
// 配置Basic登录
//.and().httpBasic()
// 配置登出页面
.and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
// 配置允许访问的链接
.and().authorizeRequests().antMatchers("/oauth/**", "/login/**", "/logout/**", "/api/**").permitAll()
// 其余所有请求全部需要鉴权认证
.anyRequest().authenticated()
// 关闭跨域保护;
.and().csrf().disable();
}
}
2.1.3 测试
测试验证令牌的端点
2.2 资源服务器
2.2.1 搭建资源服务器
pom.xml
<?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.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.lxs</groupId>
<artifactId>cms</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cms</name>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR4</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>
启动器配置文件
com\lxs\oauth2\CmsApplication.java
package com.lxs.oauth2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
@SpringBootApplication
@EnableResourceServer
public class CmsApplication {
public static void main(String[] args) {
SpringApplication.run(CmsApplication.class, args);
}
}
application.properties
server.port=8084
server.servlet.context-path=/cms
2.2.2 配置类
Oauth2资源服务器配置类
完整代码
package com.lxs.oauth2.config;
import java.io.IOException;
import javax.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
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.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
@Configuration
public class Oauth2ResourceServerConfiguration extends
ResourceServerConfigurerAdapter {
private static final String CHECK_TOKEN_URL = "http://localhost:8888/oauth/check_token";
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
RemoteTokenServices tokenService = new RemoteTokenServices();
// tokenService.setRestTemplate(restTemplate);
tokenService.setCheckTokenEndpointUrl(CHECK_TOKEN_URL);
tokenService.setClientId("cms");
tokenService.setClientSecret("secret");
// DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
// defaultAccessTokenConverter.setUserTokenConverter(new CustomUserAuthenticationConverter());
// tokenService.setAccessTokenConverter(defaultAccessTokenConverter);
resources.tokenServices(tokenService);
}
}
spring security配置类
package com.lxs.oauth2.config;
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;
/**
* @Des 乐购商城项目
* @Author 雪松
* @Date 2020/10/14 14:13
*/
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").authenticated();
// 禁用CSRF
http.csrf().disable();
}
}
2.2.3 Controller
1个Controller用于测试
package com.lxs.oauth2.controller;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
/**
* @Des 乐购商城项目
* @Author 雪松
* @Date 2020/10/14 15:05
*/
@RestController
public class HelloController {
@GetMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication) {
return authentication;
}
@GetMapping("/index")
public String index() {
return "index";
}
}
2.2.4 测试
3 Oauth2授权模式
Oauth2有以下授权模式回顾:
OAuth 协议的授权模式共分为 4 种,分别说明如下:
- 授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本 都是使用这种模式。
- 简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器中请令牌,一般若网站是纯静态页面,则可以采用这种方式。
- 密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器中请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。
- 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。
3.1 授权码模式
3.1.1 授权码模式流程
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌1。授权码模式功能最完整、使用最广泛、流程最严密的授权模式
第三方授权一般就是授权码模式,流程如下:
- (A):客户端携带client_id、redirect_uri,中间通过代理者访问授权服务器,如果已经登录过会直接返回redirect_uri,没有登录过就跳转到登录页面
- (B)授权服务器对客户端进行身份验证(通过用户代理,让用户输入用户名和密码)
- (C)授权通过,会重定向到redirect_uri并携带授权码code作为uri参数
- (D)客户端携带授权码访问授权服务器
- (E)验证授权码通过,返回acceptToken
3.1.2 认证服务授权码配置
4.1.3 功能测试
(1)申请授权码
访问授权链接,在浏览器访问就可以,授权码模式response_type参数传code:
Get请求:
http://localhost:8888/oauth/authorize?client_id=cms&client_secret=secret&response_type=code
参数列表如下:
client_id:客户端id,和授权配置类中设置的客户端id一致。
response_type:授权码模式固定为code
scop:客户端范围,和授权配置类中设置的scop一致。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)
因为没登录,所以会返回SpringSecurity的默认登录页面,具体代码是http .formLogin().permitAll();
,如果要弹窗登录的,可以配置http.httpBasic();
,这种配置是没有登录页面的,自定义登录页面可以这样配置http.formLogin().loginPage("/login").permitAll()
,参考OAuth2Config代码
如图,输入SpringSecurity配置的静态账号密码:lxs/123
登录成功,返回redirect_uri,拿到授权码
(2)申请令牌
拿到授权码后,申请令牌。 使用Postman申请授权码
grant_type:授权类型,填写authorization_code,表示授权码模式
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
Base64(cms:secret) = WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA=
此链接需要使用 http Basic认证。 什么是http Basic认证? http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用base64编 码,放在header中请求服务端,一个例子: Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA= 是用户名:密码的base64编码。 认证失败服务端返回 401 Unauthorized。
以上测试使用postman完成:
http basic认证:
客户端Id和客户端密码会匹配数据库oauth_client_details表中的客户端id及客户端密码。
点击发送: 申请令牌成功
返回信如下:
access_token:访问令牌,携带此令牌访问资源
token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer Token(http://www.rfcreader.com/#rfc6750)。
refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
expires_in:过期时间,单位为秒。
scope:范围,与定义的客户端范围一致。
jti:当前token的唯一标识
(3)令牌校验
Spring Security Oauth2提供校验令牌的端点,如下:
Get:
http://localhost:8888/oauth/check_token?token=171ce96e-7492-4a27-becd-8ccbdc69666b
(4)使用令牌
使用正确令牌访问/index服务
不用令牌访问/index服务
使用错误令牌访问
3.2 简化模式
3.2.1 简化模式流程
简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此称简化模式。简化模式是相对于授权码模式而言的
简化模式,流程如下:
- (A):客户端携带client_id、redirect_uri,中间通过代理者访问授权服务器,如果已经登录过会直接返回redirect_uri,没有登录过就跳转到登录页面
- (B)授权服务器对客户端进行身份验证(通过用户代理,让用户输入用户名和密码)
- (C)授权通过,会重定向到redirect_uri并携带授权码token作为uri参数
- (D)客户端携带授权码访问资源服务器
- (E)验证token通过,返回资源
3.2.2 认证服务授权码配置
3.2.3 功能测试
(1)申请令牌
访问授权链接,在浏览器访问就可以,授权码模式response_type参数传token:
Get请求:
http://localhost:8888/oauth/authorize?client_id=cms&redirect_uri=http://127.0.0.1:8084/cms/login&response_type=token&scope=all
参数列表如下:
client_id:客户端id,和授权配置类中设置的客户端id一致。
response_type:简化模式固定为token
scop:客户端范围,和授权配置类中设置的scop一致。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址
因为没登录,所以会返回SpringSecurity的默认登录页面,具体代码是http .formLogin().permitAll();
,如果要弹窗登录的,可以配置http.httpBasic();
,这种配置是没有登录页面的,自定义登录页面可以这样配置http.formLogin().loginPage("/login").permitAll()
,参考OAuth2Config代码
如图,输入SpringSecurity配置的静态账号密码:lxs/123
登录成功,返回redirect_uri,直接拿到令牌
(2)令牌校验
Spring Security Oauth2提供校验令牌的端点,如下:
Get:
http://localhost:8888/oauth/check_token?token=171ce96e-7492-4a27-becd-8ccbdc69666b
(3)使用令牌
使用正确令牌访问/index服务
不用令牌访问/index服务
使用错误令牌访问
简化模式和授权码模式的区别
授权码模式User-agent(浏览器)只是持有授权码(code)使用授权码获得令牌,授权码,只能校验一次,这样即使授权码泄露,令牌相对安全,而简化模式由user agent(浏览器),直接持有令牌,相对不安全
3.3 密码模式
密码模式(resource owner password credentials):密码模式中,用户向客户端提供自己的用户名和密码,这通常用在用户对客户端高度信任的情况
密码授权一般就是授权码模式,流程如下:
- (A)用户访问客户端,提供URI连接包含用户名和密码信息给授权服务器
- (B)授权服务器对客户端进行身份验证
- (C)授权通过,返回acceptToken给客户端
3.3.1 密码模式配置
3.3.2 功能测试
(1)申请令牌
grant_type:授权类型,填写password,表示密码模式
username:用户名
password:密码
此链接需要使用 http Basic认证。 什么是http Basic认证? http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用base64编 码,放在header中请求服务端,一个例子: Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA= 是用户名:密码的base64编码。 认证失败服务端返回 401 Unauthorized。
以上测试使用postman完成:
http basic认证:
客户端Id和客户端密码会匹配数据库oauth_client_details表中的客户端id及客户端密码。
点击发送: 申请令牌成功
返回信如下:
access_token:访问令牌,携带此令牌访问资源
token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer Token(http://www.rfcreader.com/#rfc6750)。
refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
expires_in:过期时间,单位为秒。
scope:范围,与定义的客户端范围一致。
jti:当前token的唯一标识
(2)令牌校验
Spring Security Oauth2提供校验令牌的端点,如下:
Get:
http://localhost:8888/oauth/check_token?token=1e628350-5711-4983-9b10-da7a7e8b9558
(3)使用令牌
使用正确令牌访问/index服务
不用令牌访问/index服务
使用错误令牌访问
3.4 客户端模式
3.4.1 客户端模式流程
客户端模式(client credentials):客户端模式(client credentials)适用于没有前端的命令行应用,即在命令行下请求令牌
客户端模式,流程如下:
- 第一步: 获取token
http://localhost:8888/oauth/token?client_id=cms&client_secret=secret&grant_type=client_credentials&scope=all
- 第二步:拿到acceptToken之后,就可以直接访问资源
3.4.2 认证服务授权码配置
3.4.3 功能测试
(1)申请令牌
访问授权链接,在浏览器访问就可以,授权码模式response_type参数传token:
post请求:
http://localhost:8888/oauth/token?client_id=cms&client_secret=secret&grant_type=client_credentials&scope=all
参数列表如下:
client_id:客户端id,和授权配置类中设置的客户端id一致。
client_secret:客户端秘钥,和授权配置类中设置的客户端secret一致。
response_type:密码模式固定为client_credentials
scop:客户端范围,和授权配置类中设置的scop一致。
(2)令牌校验
Spring Security Oauth2提供校验令牌的端点,如下:
Get:
http://localhost:8888/oauth/check_token?token=171ce96e-7492-4a27-becd-8ccbdc69666b
(3)使用令牌
使用正确令牌访问/index服务
不用令牌访问/index服务
使用错误令牌访问
这种模式跟密码模式一样,同样适用于前端分离的微服务架构中实现SSO
3.5 令牌存储方式
对于token存储有如下方式,分别进行介绍:
- InMemoryTokenStore,默认存储,保存在内存
- JdbcTokenStore,access_token存储在数据库
- JwtTokenStore,JWT这种方式比较特殊,这是一种无状态方式的存储,不进行内存、数据库存储,只是JWT中携带全面的用户信息,保存在jwt中携带过去校验就可以,系统中采用JwtTokenStore
- RedisTokenStore,将 access_token 存到 redis 中。
- JwkTokenStore,将 access_token 保存到 JSON Web Key。
如果实现无状态,认证服务器不存储Token,可以采用JwtTokenStore,我们的乐购商城项目中采用的是JWT方式存储token,如果想使用redis或者jdbc数据库存储token,可以参考网上的文章,这里不再讲解
4 商城项目授权中心微服务
授权中心微服务使用密码授权模式,颁发令牌。
乐购商城项目的Oauth2认证服务器采用JwtTokenStore处理token,也就是认证服务器器不存储token,实现纯粹的无状态token管理,同时应该spring security oauth2的脚本规范,使用数据库方式存储客户端信息
4.1 导入数据库脚本
项目中使用数据库脚本存储客户端信息
4.2 搭建项目
创建auth-center授权中心微服务工程,采用密码模式颁发令牌授权
<?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>legou-parent</artifactId>
<groupId>com.lxs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>auth-center</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<!--oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.lxs</groupId>
<artifactId>legou-security-instance</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>cert</nonFilteredFileExtension>
<nonFilteredFileExtension>jks</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
</plugins>
</build>
</project>
auth-center/src/main/java/com/service/auth/serviceauth/AuthApplication.java
package com.service.auth.serviceauth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
@SpringBootApplication
@EnableResourceServer
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
auth-center/src/main/resources/bootstrap.yml
spring:
application:
name: auth-center
main:
allow-bean-definition-overriding: true
#logging:
# level:
# root: debug
config-repo/auth-center.yml
server:
port: 9098
logging:
level:
org.springframework.security: DEBUG
4.3 自定义UserDetailsService
我们颁发的令牌,会携带用户信息,包括角色,这些用户信息,需要使用RBAC微服务查询获得,然后通过JWT令牌载荷存储,所以这里我们按照spring security规范实现自定义UserDetailsService,获取用户角色权限RBAC数据,存储到令牌
使用自定义的UserDetailsService访问RBAC数据
访问RBAC微服务需要使用Feign调用RBAC微服务
auth-center/src/main/java/com/service/auth/serviceauth/client/UserClient.java
package com.service.auth.serviceauth.client;
import com.lxs.legou.security.api.UserApi;
import com.lxs.legou.security.po.Role;
import com.lxs.legou.security.po.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@FeignClient(name = "security-service", fallback = UserClient.UserClientFallback.class)
public interface UserClient extends UserApi {
@Component
@RequestMapping("/fallback") //这个可以避免容器中requestMapping重复
class UserClientFallback implements UserClient {
private static final Logger LOGGER = LoggerFactory.getLogger(UserClientFallback.class);
@Override
public User getByUserName(String userName) {
LOGGER.info("异常发生,进入fallback方法");
return null;
}
@Override
public List<Role> selectRolesByUserId(Long id) {
LOGGER.info("异常发生,进入fallback方法");
return null;
}
}
}
auth-center/src/main/java/com/service/auth/serviceauth/service/impl/UserDetailServiceImpl.java
package com.service.auth.serviceauth.service.impl;
import com.lxs.legou.security.po.Role;
import com.service.auth.serviceauth.client.UserClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
//自定义userdetailservice
@Service
public class UserDetailServiceImpl implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(UserDetailServiceImpl.class);
@Autowired
UserClient userClient;
@Autowired
PasswordEncoder passwordEncoder;//BCryptPasswordEncoder
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
com.lxs.legou.security.po.User user = userClient.getByUserName(username);
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
if (user != null) {
logger.debug("current user = " + user);
//获取用户的授权
List<Role> roles = userClient.selectRolesByUserId(user.getId());
//声明授权文件
for (Role role : roles) {
if (role != null && role.getName() != null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + role.getName());//spring Security中权限名称必须满足ROLE_XXX
grantedAuthorities.add(grantedAuthority);
}
}
}
logger.debug("granted authorities = " + grantedAuthorities);
return new User(user.getUserName(), user.getPassword(), grantedAuthorities);
}
}
4.4 配置类
oauth2配置类
完整代码
auth-center/src/main/java/com/service/auth/serviceauth/config/AuthorizationServerConfiguration.java
package com.service.auth.serviceauth.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
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.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
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;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import javax.sql.DataSource;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private static final Logger logger = LoggerFactory.getLogger(AuthorizationServerConfiguration.class);
@Autowired
@Qualifier("authenticationManagerBean")
AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;//配置文件配置的数据库信息
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("lxs.jks"), "123456".toCharArray());//证书路径和密钥库密码
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("lxs"));//密钥别名
return converter;
}
@Bean//声明 ClientDetails实现
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Override//配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
/* // 配置两个客户端,一个用于password认证一个用于client认证
clients.jdbc(dataSource)
.withClient("client_1")
.authorizedGrantTypes("client_credentials", "refresh_token")
.scopes("select")
.authorities("oauth2")
.secret(new BCryptPasswordEncoder().encode("123456"))
.and().withClient("client_2")
.authorizedGrantTypes("password", "refresh_token")
.scopes("server")
.authorities("oauth2")
.secret(new BCryptPasswordEncoder().encode("123456"));*/
//直接读取数据库,需要保证数据库配置有客户端信息(oauth_client_details),否则资源服务器无法获取认证数据
clients.withClientDetails(clientDetailsService());
}
@Override//配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services),还有token的存储方式(tokenStore)
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtAccessTokenConverter()).authenticationManager(authenticationManager);
// 配置tokenServices参数
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(false);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30)); // 30天
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许表单认证
security.allowFormAuthenticationForClients()
// 开启/oauth/token_key验证端口无权限访问
.tokenKeyAccess("permitAll()")
// 开启/oauth/check_token验证端口认证权限访问
.checkTokenAccess("isAuthenticated()");
}
}
spring security配置类
完整代码
auth-center/src/main/java/com/service/auth/serviceauth/config/SecurityConfiguration.java
package com.service.auth.serviceauth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;//注入自定义userdetailservice(com.service.auth.serviceauth.service.impl.UserDetailServiceImpl)
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
//return PasswordEncoderFactories.createDelegatingPasswordEncoder();//兼容多种密码的加密方式
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers().anyRequest()//所有请求都加入HttpSecurity(多个HttpSecurity过滤)
.and().authorizeRequests().antMatchers("/oauth/**").permitAll();//开放/oauth/开头的所有请求
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());//注入自定义的UserDetailsService,采用BCrypt加密
}
}
4.6 测试
5 非对称加密
非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。 非对称加密算法实现机密信息交换的基本过程是:甲方生成一对密钥并将公钥公开,需要向甲方发送信息的其他角色(乙方)使用该密钥(甲方的公钥)对机密信息进行加密后再发送给甲方;甲方再用自己私钥对加密后的信息进行解密。甲方想要回复乙方时正好相反,使用乙方的公钥对数据进行加密,同理,乙方使用自己的私钥来进行解密。
5.1 公钥私钥
5.1.1 公钥私钥原理
张三有两把钥匙,一把是公钥,另一把是私钥。
张三把公钥送给他的朋友们----李四、王五、赵六----每人一把。
5.1.1.1 公钥加密私钥解密
注:这个过程我们项目中没有涉及和使用
李四要给张三写一封保密的信。她写完后用张三的公钥加密,就可以达到保密的效果。
张三收信后,用私钥解密,就看到了信件内容。这里要强调的是,只要张三的私钥不泄露,这封信就是安全的,即使落在别人手里,也无法解密。
5.1.1.2 私钥签名令牌公钥验证
项目中的JWT就是使用这种模式
张三给李四回信,决定采用"数字签名",注:这个过程就是JWT的签名防篡改。他写完后先用Hash函数,生成信件的摘要(digest)。张三将这个签名,附在信件下面,一起发给李四。
李四收信后,取下数字签名,用张三的公钥解密,得到信件的摘要。由此证明,这封信确实是张三发出的。李四再对信件本身使用Hash函数,将得到的结果,与上一步得到的摘要进行对比。如果两者一致,就证明这封信未被修改过。
5.2 资源服务授权流程
(1)传统授权流程
资源服务器授权流程如上图,客户端先去授权服务器申请令牌,申请令牌后,携带令牌访问资源服务器,资源服务器访问授权服务校验令牌的合法性,授权服务会返回校验结果,如果校验成功会返回用户信息给资源服务器,资源服务器如果接收到的校验结果通过了,则返回资源给客户端。
传统授权方法的问题是用户每次请求资源服务,资源服务都需要携带令牌访问认证服务去校验令牌的合法性,并根 据令牌获取用户的相关信息,性能低下。
(2)公钥私钥授权流程
传统的授权模式性能低下,每次都需要请求授权服务校验令牌合法性,我们可以利用公钥私钥完成对令牌的加密,如果加密解密成功,则表示令牌合法,如果加密解密失败,则表示令牌无效不合法,合法则允许访问资源服务器的资源,解密失败,则不允许访问资源服务器资源。
上图的业务流程如下:
1、客户端请求认证服务申请令牌
2、认证服务生成令牌认证服务采用非对称加密算法,使用私钥生成令牌。
3、客户端携带令牌访问资源服务客户端在Http header 中添加: Authorization:Bearer 令牌。
4、资源服务请求认证服务校验令牌的有效性资源服务接收到令牌,使用公钥校验令牌的合法性。
5、令牌有效,资源服务向客户端响应资源信息
5.2.2 生成私钥公钥
我们采用JWT私钥颁发令牌,公钥校验令牌,这里先试用keytool工具生成公钥私钥证书
(1)生成密钥证书 下边命令生成密钥证书,采用RSA 算法每个证书包含公钥和私钥
创建一个文件夹,在该文件夹下执行如下命令行:
keytool -genkeypair -alias lxsong -keyalg RSA -keypass lxsong -keystore lxsong.jks -storepass lxsong
Keytool 是一个java提供的证书管理工具
-alias:密钥的别名
-keyalg:使用的hash算法
-keypass:密钥的访问密码
-keystore:密钥库文件名,xc.keystore保存了生成的证书
-storepass:密钥库的访问密码
(2)查询证书信息
keytool -list -keystore lxsong.jks
(3)删除别名
keytool -delete -alias lxsong -keystore lxsong.jsk
5.2.3 导出公钥
openssl是一个加解密工具包,这里使用openssl来导出公钥信息。
安装 openssl:http://slproweb.com/products/Win32OpenSSL.html
安装资料目录下的Win64OpenSSL-1_1_0g.exe
配置openssl的path环境变量,如下图:
本教程配置在C:\OpenSSL-Win64\bin
cmd进入lxsong.jks文件所在目录执行如下命令(如下命令在windows下执行,会把-变成中文方式,请将它改成英文的-):
keytool -list -rfc --keystore lxsong.jks | openssl x509 -inform pem -pubkey
下面段内容是公钥
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAm
t47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnh
cP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEm
oLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/
iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZS
xtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv
9QIDAQAB
-----END PUBLIC KEY-----
将上边的公钥拷贝到文本public.key文件中,合并为一行,可以将它放到需要实现授权认证的工程中。
5.2.4 JWT令牌
(1)创建令牌数据
在auth-center工程中创建测试类JWTTest,使用它来创建令牌信息,代码如下:
auth-center/src/test/java/com/service/auth/serviceauth/JWTTest.java
package com.service.auth.serviceauth;
import org.codehaus.jackson.map.ObjectMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import org.springframework.test.context.junit4.SpringRunner;
import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.*;
public class JwtTest {
/**
* 使用私钥生成令牌
*/
@Test
public void testCreateJwt() throws Exception {
//存储秘钥的工厂对象
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("mickey.jks"), "mickey".toCharArray());
//密钥对(公钥-》私钥)
KeyPair keyPair = keyStoreKeyFactory.getKeyPair("mickey", "mickey".toCharArray());
//私钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
//自定义payload信息
Map<String, Object> tokenMap = new HashMap<>();
tokenMap.put("id", 123);
tokenMap.put("name", "mickey");
tokenMap.put("roles", "r01, r02, admin");
//使用工具类,通过私钥颁发JWT令牌
Jwt jwt = JwtHelper.encode(new ObjectMapper().writeValueAsString(tokenMap), new RsaSigner(privateKey));
String token = jwt.getEncoded();
System.out.println(token);
}
}
运行后的结果如下:
token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJpdGhlaW1hIiwiaWQiOiIxIn0.IR9Qu9ZqYZ2gU2qgAziyT38UhEeL4Oi69ko-dzC_P9-Vjz40hwZDqxl8wZ-W2WAw1eWGIHV1EYDjg0-eilogJZ5UikyWw1bewXCpvlM-ZRtYQQqHFTlfDiVcFetyTayaskwa-x_BVS4pTWAskiaIKbKR4KcME2E5o1rEek-3YPkqAiZ6WP1UOmpaCJDaaFSdninqG0gzSCuGvLuG40x0Ngpfk7mPOecsIi5cbJElpdYUsCr9oXc53ROyfvYpHjzV7c2D5eIZu3leUPXRvvVAPJFEcSBiisxUSEeiGpmuQhaFZd1g-yJ1WQrixFvehMeLX2XU6W1nlL5ARTpQf_Jjiw"
(2)解析令牌
上面创建令牌后,我们可以对JWT令牌进行解析,这里解析需要用到公钥,我们可以将之前生成的公钥public.key拷贝出来用字符串变量token存储,然后通过公钥解密。
在legou-user-oauth创建测试类com.legou.token.ParseJwtTest实现解析校验令牌数据,代码如下:
/**
* 使用公钥校验令牌
*/
@Test
public void testVerify() {
//令牌
String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6InIwMSwgcjAyLCBhZG1pbiIsIm5hbWUiOiJtaWNrZXkiLCJpZCI6MTIzfQ.LPQKnZmAdj_9mp8KPWjEeEfmJ2SQmbIZvR2oBQ1A8Ze1xDISY4G2IYWQcPCW7D0Y7rrQEqf1j9YikA8kQIQdybQmXEn9Jtd7HPUHgCUyVLukJ3-g34kMzCzrBDCtuNXzD3PfNElBk9FRTOnKG1_4Rzn0nVGWyFOsQcb8aTR9ch-5hTGHeJ-S_G0ttJpwAktO8x_OMQTSqAV99f0WvXtV14_e-8LoFSVKjawarmCY9tabHDudnWljA7xL-5qjSSUvUs4hoon6IwvhRRERjJU0jvUxAQVPOeXgauANkRCBEFX3Yjt63Z_UOfTzSkSQQcz5tgUBCWHk1gAeG6gYzghf3w";
//公钥
String publicKey = "-----BEGIN PUBLIC KEY-----\n" +
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhoe+ze0O4CKGc7k9U5dJ\n" +
"FJFnBeWh3Pcx8VDuL+SXIzGDKADMU2zo1f/80pvCCuXYqHhu5CQ3wzeBGx7BvR3v\n" +
"jeuWnp5GQArAplOCaDDFKfD8Dxyq9kaCSUn6IBX33k3uMbWmJqACd9gEq1eJWaQL\n" +
"mh46eAn2Kvb5i1UZH3t6MrOIbyPIva59BZsvhek4ZsPxdzC3SgbKqxIlIF69bSoh\n" +
"M76fJnJGYnpNRg3CmbfEl/no3dEYST/4WgWVJ9M0DIjqN4tbWN7xrKzcXAX9Y4mh\n" +
"IJvnxh88RfKBPQD5X04a04OHYusjczoy8quVkr/agRlyeuLnFOJolmGgjBxU1nLn\n" +
"cQIDAQAB\n" +
"-----END PUBLIC KEY-----";
//校验令牌
Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publicKey));
String claims = jwt.getClaims();
System.out.println(claims);
}
运行后的结果如下:
{"ext":"1","roles":"r01,r02","name":"mrt","id":"123"}
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHQiOiIxIiwicm9sZXMiOiJyMDEscjAyIiwibmFtZSI6Im1ydCIsImlkIjoiMTIzIn0.OXzFObxUq35--qgvBy4mnBXx-f9mfpYMczTfAfH7yHM05W-oJ6RPmLPonZsFlZMd8JBdLm6iz_TN6b4ynO0heCBsyML2ZLx0sxhgE28mhztDXj2GHbWu3kwsRzU9Pbgy-CO3FIG0Iw-aIkFSivaaLsCju5oOLxGB825ueI5hM58sPLLykZPAaU6DcVY3X1sfpWDIQ7G7JkCoP3rH385Vcmg1VBJVIwVxEn4TXHtWqre9lgK-T7D4zXlhScB57gv9OfcbebNm8tI2Rew1IHmOCeKf5CKAiSCv5d26LhLPKqvGBQ5Cy67JM58X2T-4LvgQeQR6TZmiiSr7fLnEkNe9KQ
6 商城项目资源服务器
6.1 资源服务器配置
基本上所有微服务都是资源服务
授权服务器采用私钥加密生成令牌,对外向资源服务器提供公钥,资源服务器使 用公钥 来校验令牌的合法性。 将公钥拷贝到 public.key文件中,将此文件拷贝到每一个需要的资源服务工程的classpath下 ,例如:RBAC微服务.
6.1.2 RBAC微服务资源授权
将上面生成的公钥public.key拷贝到legou-security-service微服务工程的resources目录下,如下图:
引入依赖
在legou-security-service微服务工程pom.xml中引入oauth依赖
<!--oauth依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
配置文件
配置访问授权服务器的公钥的端点
legou-parent\config-repo\security-service.yml
security:
oauth2:
resource:
jwt:
key-uri: http://localhost:9098/oauth/token_key #如果使用JWT,可以获取公钥用于 token 的验签
配置使用公钥校验令牌
legou-security/legou-security-service/src/main/java/com/lxs/legou/security/config/JwtConfig.java
package com.lxs.legou.security.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
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;
import org.springframework.util.FileCopyUtils;
import java.io.IOException;
@Configuration
public class JwtConfig {
public static final String public_cert = "public.key";
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Bean
@Qualifier("tokenStore")
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Bean
protected JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource(public_cert);
String publicKey;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
}catch (IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey); //设置校验公钥
converter.setSigningKey("lxsong"); //设置证书签名密码,否则报错
return converter;
}
}
配置资源服务器
legou-security/legou-security-service/src/main/java/com/lxs/legou/security/config/ResourceServerConfiguration.java
package com.lxs.legou.security.config;
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;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)//激活方法上的PreAuthorize注解
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/user/**", "/security/user/**").permitAll()
.antMatchers("/book/**").hasRole("ADMIN") //用于测试
.antMatchers("/**").authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore);
}
}
6.1.3 授权测试
用户每次访问微服务的时候,需要先申请令牌,令牌申请后,每次将令牌放到头文件中,才能访问微服务。
头文件中每次需要添加一个Authorization
头信息,头的结果为bearer token
。
(1) 创建测试用的Controller
package com.lxs.legou.security.controller;
import com.lxs.legou.security.utils.BPwdEncoderUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
public class TestEndPointController {
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
return "product id : " + id;
}
@GetMapping("/order/{id}")
public String getOrder(@PathVariable String id) {
return "order id : " + id;
}
@GetMapping("/book/{id}")
public String getBook(@PathVariable String id) {
return "book id : " + id;
}
@GetMapping("/getPrinciple")
public OAuth2Authentication getPrinciple(OAuth2Authentication oAuth2Authentication, Principal principal, Authentication authentication) {
logger.info(oAuth2Authentication.getUserAuthentication().getAuthorities().toString());
logger.info(oAuth2Authentication.toString());
logger.info("principal.toString() " + principal.toString());
logger.info("principal.getName() " + principal.getName());
logger.info("authentication: " + authentication.getAuthorities().toString());
return oAuth2Authentication;
}
}
(2)不携带令牌测试
访问http://localhost:18089/order/1 不携带令牌,结果如下:
(2)携带正确令牌访问
访问http://localhost:9002/order/123 携带正确令牌,结果如下:
(3)携带错误令牌
访问http://localhost:9002/order/123 携带不正确令牌,结果如下:
6.2 SpringSecurity权限控制
由于我们项目使用了微服务,任何用户都有可能使用任意微服务,此时我们需要控制相关权限,例如:普通用户角色不能使用用户的删除操作,只有管理员才可以使用,那么这个时候就需要使用到SpringSecurity的权限控制功能了。
6.2.1 角色加载
在auth-center服务中,auth-center/src/main/java/com/service/auth/serviceauth/service/impl/UserDetailServiceImpl.java该类实现了加载用户相关信息,如下代码:
数据库中的数据如下:
只用id=15的用户admin属于管理员
6.2.2 角色权限控制
在每个微服务中,需要获取用户的角色,然后根据角色识别是否允许操作指定的方法,Spring Security中定义了四个支持权限控制的表达式注解,分别是@PreAuthorize
、@PostAuthorize
、@PreFilter
和@PostFilter
。其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。在需要控制权限的方法上,我们可以添加@PreAuthorize
注解,用于方法执行前进行权限检查,校验用户当前角色是否能访问该方法。
(1)开启@PreAuthorize
在legou-security
的ResourceServerConfiguration类上添加@EnableGlobalMethodSecurity
注解,用于开启@PreAuthorize的支持,代码如下:
(2)方法权限控制
在legou-security
微服务的TestEndPointController
类的测试方法上添加权限控制注解@PreAuthorize
,代码如下:
同时也可以在资源服务配置类中配置,进行权限控制,如下图:
(3)测试
我们使用Postman测试,分别获得管理员和刘国梁的令牌,其中管理员用户属于管理员角色,刘国梁不属于管理员角色
这时使用刘国梁用户的令牌访问/anno/123,拒绝访问
发现上面无法访问,因为用户登录的时候,刘国梁用户包含admin角色,而anno方法需要admin角色,所以被拦截了。
我们再测试其他方法,其他方法没有配置拦截,所以用户登录后就会放行。如下
访问http://localhost:9002/order/123
效果如下:
使用管理员用户访问/anno/123,则有权限放行可以访问
知识点说明:
如果希望一个方法能被多个角色访问,配置:@PreAuthorize("hasAnyAuthority('admin','user')")
如果希望一个类都能被多个角色访问,在类上配置:@PreAuthorize("hasAnyAuthority('admin','user')")
7 登录实现
7.1 登录流程
登录流程
- 前端提交用户名、密码
- 用户微服务得到用户名密码
- 用户微服务组织数据包括:client_id:client_secrect组织Basic Authorization,用户名,密码等参数
- 用户微服务使用restTemplate发送HTTP请求给授权中心微服务
- 授权中心微服务校验通过颁发令牌
- 前端将令牌令牌存储到sessionStorage中,下次访问资源服务器通过Header携带访问
7.2 配置文件
在用户微服务工程中配置
pom.xml中增加oauth构件依赖
<!--oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
legou-security.yml中增加如下配置
配置文件完整代码
server:
port: 9002
mybatis-plus:
mapper-locations: classpath*:mybatis/*/*.xml
type-aliases-package: com.lxs.legou.*.po
configuration:
# 下划线驼峰转换
map-underscore-to-camel-case: true
lazy-loading-enabled: true
aggressive-lazy-loading: false
logging:
#file: demo.log
pattern:
console: "%d - %msg%n"
level:
org.springframework.web: debug
com.lxs: debug
security:
oauth2:
resource:
jwt:
key-uri: http://localhost:9098/oauth/token_key #如果使用JWT,可以获取公钥用于 token 的验签
client:
access-token-uri: http://localhost:9098/oauth/token #令牌端点
user-authorization-uri: http://localhost:9098/oauth/authorize #授权端点
client-id: client
client-secret: 123456
grant-type: password
scope: read,write
UserController对应的读取配置信息的对象
7.3 配置类
在spring容器中创建BCryptPasswordEncoder加密加密组件,处理密码
package com.lxs.legou.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
//return PasswordEncoderFactories.createDelegatingPasswordEncoder();//兼容多种密码的加密方式
}
}
7.4 业务层
保存用户使用BCryptPasswordEncoder处理密码
7.5 控制层
package com.lxs.legou.security.controller;
import com.lxs.legou.core.controller.BaseController;
import com.lxs.legou.core.po.ResponseBean;
import com.lxs.legou.security.dto.UserLoginParamDto;
import com.lxs.legou.security.po.Role;
import com.lxs.legou.security.po.User;
import com.lxs.legou.security.service.IUserService;
import com.lxs.legou.security.utils.BPwdEncoderUtil;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.OAuth2ClientProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import javax.validation.Valid;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
@RestController
@RequestMapping(value = "/user")
public class UserController extends BaseController<IUserService, User> {
@Autowired
private OAuth2ClientProperties oAuth2ClientProperties;
@Autowired
private OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails;
@Autowired
private RestTemplate restTemplate;
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@RequestMapping("/login")
public ResponseEntity<OAuth2AccessToken> login(String username, String password) {
//1:验证用户
User user = service.getUserByUserName(username);
if (null == user) {
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}
if (!BPwdEncoderUtil.matches(password, user.getPassword())) {
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}
//2:使用restTemplate发送请求到授权服务器,申请令牌
//请求头 "basic auth"
String client_secret = oAuth2ClientProperties.getClientId() + ":" + oAuth2ClientProperties.getClientSecret();
client_secret = "Basic " + Base64.getEncoder().encodeToString(client_secret.getBytes());
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", client_secret);
//请求参数
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.put("username", Collections.singletonList(username));
map.put("password", Collections.singletonList(password));
map.put("grant_type", Collections.singletonList(oAuth2ProtectedResourceDetails.getGrantType()));
map.put("scope",oAuth2ProtectedResourceDetails.getScope());
//HttpEntity(请求参数,头。。。)
HttpEntity httpEntity = new HttpEntity(map, headers);
return restTemplate.exchange(oAuth2ProtectedResourceDetails.getAccessTokenUri(), HttpMethod.POST, httpEntity, OAuth2AccessToken.class);
}
@ApiOperation("通过登录获得用户")
@GetMapping("/get/{userName}")
public User getByUserName(@PathVariable("userName") String userName) {
return service.getUserByUserName(userName);
}
@ApiOperation("通过用户ID获得角色")
@GetMapping("/select-roles/{id}")
public List<Role> selectRolesByUserId(@PathVariable("id") Long id) {
return service.selectRoleByUser(id);
}
/**
* 验证用户名是否存在
*/
@ApiOperation("验证用户名是否存在")
@PostMapping("/validate-name/{userName}")
public String validUserName(@PathVariable String userName, Long id) {
long rowCount = service.findCountByUserName(userName);
//修改时=原来的用户名
if (id != null) {
User user = service.getById(id);
if (null != userName && userName.equals(user.getUserName())) {
return "{\"success\": true}";
}
}
if (rowCount > 0) {
return "{\"success\": false}";
} else {
return "{\"success\": true}";
}
}
/**
* 锁定用户
*/
@GetMapping("/lock/{id}")
@ApiOperation("锁定账户")
public ResponseBean lock(@PathVariable Long id) throws Exception {
ResponseBean rm = new ResponseBean();
try {
User u = service.getById(id);
User user = new User();
user.setId(id);
if (null != u.getLock() && u.getLock()) {
rm.setMsg("用户已解锁");
user.setLock(false);
} else {
rm.setMsg("用户已锁定");
user.setLock(true);
}
service.updateById(user);
} catch (Exception e) {
e.printStackTrace();
rm.setSuccess(false);
rm.setMsg("保存失败");
}
return rm;
}
@Override
public void afterEdit(User domain) {
//生成角色列表, 如:1,3,4
List<Role> roles = service.selectRoleByUser(domain.getId());
Long[] ids = new Long[roles.size()];
for (int i=0; i< roles.size(); i++) {
ids[i] = roles.get(i).getId();
}
domain.setRoleIds(ids);
}
}
7.6 测试登录
使用postman测试:
Post请求:http://localhost:9002/user/login
9 前端认证授权实现
商城后台管理项目认证和授权基本逻辑如下:
- 登录时输入用户名、密码参数访问认证服务器
- 登录成功后,认证服务器返回的token,存储在sessionStorate中
- 访问其他微服务(如用户,商品,订单,支付微服务),使用Header写到token到这些资源服务器,格式:Bearer token
逻辑图如下:
我们本着轻前端原则,不用重新编写,明白逻辑,能够出现问题,修改就可以,有兴趣的同学可以参考标准代码,自行实现
9.1 登录
src/view/login/login.vue
9.2 路由前置全局守卫
这个前端组件有点像springmvc的前置拦截器。官方文档的解释:
正如其名,vue-router
提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。
记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route
对象来应对这些变化,或使用 beforeRouteUpdate
的组件内守卫。
src/router/index.js
9.3 HTTP协议数据分析
9.3.1 登录
使用Fiddler查看登录HTTP协议数据如下:
9.3.2 访问用户微服务
使用Fiddler查看访问用户微服务的HTTP协议数据如下: