Oauth2授权码模式
授权码授权流程
例举的黑马程序员网站使用微信认证的过程就是授权码模式,流程如下:
1、客户端请求第三方授权
2、用户(资源拥有者)同意给客户端授权
3、客户端获取到授权码,请求认证服务器申请令牌
4、认证服务器向客户端响应令牌
5、客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权
6、资源服务器返回受保护资源
申请授权码
请求认证服务获取授权码:
Get请求:
localhost:40400/auth/oauth/authorize?client_id=XcWebApp&response_type=code&scop=app&redirect_uri=http://localhost
参数列表如下:
client_id:客户端id,和授权配置类中设置的客户端id一致。
response_type:授权码模式固定为code
scop:客户端范围,和授权配置类中设置的scop一致。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。
数据库中有一张表,表里面存放了配置的客户端id,如果请求的参数后的client_id在数据库中不存在,是不会颁发授权码的
请求后会跳转到登录页面
用户名是client_id,密码是client_secret(已经加密过)
密码和账号输入成功后会带着授权码重定向到指定的uri下
申请令牌
拿到授权码后,申请令牌。Post请求
http://localhost:40400/auth/oauth/token
参数如下:
grant_type:授权类型,填写authorization_code,表示授权码模式
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
此链接需要使用 http Basic认证。
使用postMan测试,返回令牌
key_store存储共钥和私钥
资源服务授权流程
资源服务拥有要访问的受保护资源,客户端携带令牌访问资源服务,如果令牌合法则可成功访问资源服务中的资源,如下图:
1、客户端请求认证服务申请令牌
2、认证服务生成令牌 认证服务采用非对称加密算法,使用私钥生成令牌。
3、客户端携带令牌访问资源服务 客户端在Http header 中添加: Authorization:Bearer 令牌。
4、资源服务请求认证服务校验令牌的有效性 资源服务接收到令牌,使用公钥校验令牌的合法性。
5、令牌有效,资源服务向客户端响应资源信息
资源服务授权配置
1、配置公钥
认证服务生成令牌采用非对称加密算法,认证服务采用私钥加密生成令牌,对外向资源服务提供公钥,资源服务使 用公钥 来校验令牌的合法性。
将公钥拷贝到 publickey.txt文件中,将此文件拷贝到资源服务工程的classpath下

2、添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring‐cloud‐starter‐oauth2</artifactId> </dependency>
4、在config包下创建ResourceServerConfig类:
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解 public class ResourceServerConfig extends ResourceServerConfigurerAdapter { //公钥 private static final String PUBLIC_KEY = "publickey.txt"; //定义JwtTokenStore ,使用jwt令牌 @Bean public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) { return new JwtTokenStore(jwtAccessTokenConverter); } //定义JJwtAccessTokenConverter,使用jwt令牌 @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setVerifierKey(getPubKey()); return converter; } /** * 获取非对称加密公钥 Key * @return 公钥 Key */ private String getPubKey() { Resource resource = new ClassPathResource(PUBLIC_KEY); try { InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream()); BufferedReader br = new BufferedReader(inputStreamReader); return br.lines().collect(Collectors.joining(" ")); } catch (IOException ioe) { return null; } } //Http安全配置,对每个到达系统的http请求链接进行校验 @Override public void configure(HttpSecurity http) throws Exception { //所有请求必须认证通过http.authorizeRequests().anyRequest().authenticated(); } }
资源服务授权测试
请求时没有携带令牌则报错:
{ "error": "unauthorized", "error_description": "Full authentication is required to access this resource" }
使用:在http header中添加 Authorization: Bearer 令牌(固定格式)

解决swagger-ui无法访问
修改授权配置类ResourceServerConfig的configure方法: 针对swagger-ui的请求路径进行放行:
//Http安全配置,对每个到达系统的http请求链接进行校验 @Override public void configure(HttpSecurity http) throws Exception { //所有请求必须认证通过 http.authorizeRequests() //下边的路径放行 .antMatchers("/v2/api‐docs", "/swagger‐resources/configuration/ui", "/swagger‐resources","/swagger‐resources/configuration/security", "/swagger‐ui.html","/webjars/**").permitAll() .anyRequest().authenticated(); }
注意: 通过上边的配置虽然可以访问swagger-ui,但是无法进行单元测试,除非去掉认证的配置或在上边配置中添加所有请求均放行("/**")。
Oauth2密码模式授权
密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接通过用户名和密码即可申请令牌。
测试如下:Post请求:http://localhost:40400/auth/oauth/token参数:
grant_type:密码模式授权填写password username:账号
password:密码
并且此链接需要使用 http Basic认证。
注意:当令牌没有过期时同一个用户再次申请令牌则不再颁发新令牌。
ps:校验令牌
Get: http://localhost:40400/auth/oauth/check_token?token=
返回结果
{ "companyId": null, "userpic": null, "user_name": "mrt", "scope": [ "app" ], "name": null, "utype": null, "id": null, "exp": 1531254828, "jti": "6a00f227‐4c30‐47dc‐a959‐c0c147806462", "client_id": "XcWebApp" }
exp:过期时间,long类型,距离1970年的秒数(new Date().getTime()可得到当前时间距离1970年的毫秒数)。
user_name: 用户名
client_id:客户端Id,在oauth_client_details中配置
scope:客户端范围,在oauth_client_details表中配置
jti:与令牌对应的唯一标识
companyId、userpic、name、utype、id:这些字段是本认证服务在Spring Security基础上扩展的用户身份信息
ps:刷新令牌
刷新令牌是当令牌快过期时重新生成一个令牌,它于授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码
也不需要账号和密码,只需要一个刷新令牌、客户端id和客户端密码。
测试如下:
Post:http://localhost:40400/auth/oauth/token
参数:
grant_type: 固定为 refresh_token
refresh_token:刷新令牌(注意不是access_token,而是refresh_token)
刷新令牌成功,会重新生成新的访问令牌和刷新令牌,令牌的有效期也比旧令牌长。刷新令牌通常是在令牌快过期时进行刷新。
JWT研究
传统校验令牌的方法
传统授权方法的问题是用户每次请求资源服务,资源服务都需要携带令牌访问认证服务去校验令牌的合法性,并根据令牌获取用户的相关信息,性能低下。
使用JWT
思路是,用户认证通过会得到一个JWT令牌,JWT令牌中已经包括了用户相关的信息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。
什么是JWT?
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于 在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公 钥/私钥对来签名,防止被篡改
JWT令牌的优点:
1、jwt基于json,非常方便解析。2、可以在令牌中自定义丰富的内容,易扩展。3、通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。4、资源服务使用JWT可不依赖认证服务即可完成授权。
缺点:
1、JWT令牌较长,占存储空间比较大。
JWT令牌结构
JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
- Header
头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA) 一个例子如下:下边是Header部分的内容
{ "alg": "HS256", "typ": "JWT" }
将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
- Payload
第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比 如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。 最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。 一个例子:
{ "sub": "1234567890", "name": "456", "admin": true }
- Signature
第三部分是签名,此部分用于防止jwt内容被篡改。这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明 签名算法进行签名。
一个例子:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
base64UrlEncode(header):jwt令牌的第一部分。base64UrlEncode(payload):jwt令牌的第二部分。secret:签名所使用的密钥。
普通令牌只是唯一标识了用户信息,资源服务只能通过请求认证服务来获取用户信息,而jwt令牌已经存储了用户信息,只要资源服务解析这个令牌就可以拿到用户信息
JWT入门
Spring Security 提供对JWT的支持,本节我们使用Spring Security 提供的JwtHelper来创建JWT令牌,校验JWT令牌等操作。
生成私钥和公钥
JWT令牌生成采用非对称加密算法
1、生成密钥证书
下边命令生成密钥证书,采用RSA 算法每个证书包含公钥和私钥
keytool -genkeypair -alias xckey -keyalg RSA -keypass xuecheng -keystore xc.keystore -storepass xuechengkeystore
Keytool 是一个java提供的证书管理工具
-alias:密钥的别名
-keyalg:使用的hash算法
-keypass:密钥的访问密码
-keystore:密钥库文件名,xc.keystore保存了生成的证书
-storepass:密钥库的访问密码
查询证书信息:
keytool -list -keystore xc.keystore
删除别名
keytool -delete -alias xckey -keystore xc.keystore
2、导出公钥
openssl是一个加解密工具包,这里使用openssl来导出公钥信息。 安装 openssl:http://slproweb.com/products/Win32OpenSSL.html,配置openssl的path环境变量(mac自带)
cmd进入xc.keystore文件所在目录执行如下命令
keytool ‐list ‐rfc ‐‐keystore xc.keystore | openssl x509 ‐inform pem ‐pubkey
将下面的红色框文字复制成一行,保存一个文件(publickey.txt),这个文件用于放到资源服务中
生成jwt令牌
在认证工程创建测试类,测试jwt令牌的生成与验证。
//生成一个jwt令牌 @Test public void testCreateJwt(){ //证书文件 String key_location = "xc.keystore"; //密钥库密码 String keystore_password = "xuechengkeystore"; //访问证书路径 ClassPathResource resource = new ClassPathResource(key_location); //密钥工厂 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource, keystore_password.toCharArray()); //密钥的密码,此密码和别名要匹配 String keypassword = "xuecheng"; //密钥别名 String alias = "xckey"; //密钥对(密钥和公钥) KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypassword.toCharArray()); //私钥 RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate(); //定义payload信息 Map<String, Object> tokenMap = new HashMap<>(); tokenMap.put("id", "123"); tokenMap.put("name", "mrt"); tokenMap.put("roles", "r01,r02"); tokenMap.put("ext", "1"); //生成jwt令牌 Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(aPrivate)); //取出jwt令牌 String token = jwt.getEncoded(); System.out.println("token="+token); }
验证jwt令牌
//资源服务使用公钥验证jwt的合法性,并对jwt解码
@Test public void testVerify(){
//jwt令牌(由上面生成) String token ="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHQiOiIxIiwicm9sZXMiOiJyMDEscjAyIiwibmFtZSI6Im1ydCIsI mlkIjoiMTIzIn0.KK7_67N5d1Dthd1PgDHMsbi0UlmjGRcm_XJUUwseJ2eZyJJWoPP2IcEZgAU3tUaaKEHUf9wSRwaDgwhrw fyIcSHbs8oy3zOQEL8j5AOjzBBs7vnRmB7DbSaQD7eJiQVJOXO1QpdmEFgjhc_IBCVTJCVWgZw60IEW1_Lg5tqaLvCiIl26K 48pJB5f‐le2zgYMzqR1L2LyTFkq39rG57VOqqSCi3dapsZQd4ctq95SJCXgGdrUDWtD52rp5o6_0uq‐ mrbRdRxkrQfsa1j8C5IW2‐T4eUmiN3f9wF9JxUK1__XC1OQkOn‐ZTBCdqwWIygDFbU7sf6KzfHJTm5vfjp6NIA"; //公钥 String publickey = "‐‐‐‐‐BEGIN PUBLIC KEY‐‐‐‐‐ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAijyxMdq4S6L1Af1rtB8SjCZHNgsQG8JTfGy55eYvzG0B/E4AudR2 prSRBvF7NYPL47scRCNPgLnvbQczBHbBug6uOr78qnWsYxHlW6Aa5dI5NsmOD4DLtSw8eX0hFyK5Fj6ScYOSFBz9cd1nNTvx 2+oIv0lJDcpQdQhsfgsEr1ntvWterZt/8r7xNN83gHYuZ6TM5MYvjQNBc5qC7Krs9wM7UoQuL+s0X6RlOib7/mcLn/lFLsLD dYQAZkSDx/6+t+1oHdMarChIPYT1sx9Dwj2j2mvFNDTKKKKAq0cv14Vrhz67Vjmz2yMJePDqUi0JYS2r0iIo7n8vN7s83v5u OQIDAQAB‐‐‐‐‐END PUBLIC KEY‐‐‐‐‐"; //校验jwt(如果jwt令牌错误,执行下面的代码会报错) Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey)); //获取jwt原始内容 String claims = jwt.getClaims(); //jwt令牌 String encoded = jwt.getEncoded(); System.out.println(encoded); }
认证接口开发
执行流程:
1、用户登录,请求认证服务
2、认证服务认证通过,生成jwt令牌,将jwt令牌及相关信息写入Redis,并且将身份令牌写入cookie
3、用户访问资源页面,带着cookie到网关
4、网关从cookie获取token,并查询Redis校验token,如果token不存在则拒绝访问,否则放行
5、用户退出,请求认证服务,清除redis中的token,并且删除cookie中的token使用redis存储用户的身份令牌有以下作用:
1、实现用户退出注销功能,服务端清除令牌后,即使客户端请求携带token也是无效的。
2、由于jwt令牌过长,不宜存储在cookie中,所以将jwt令牌存储在redis,由客户端请求服务端获取并在客户端存储。
spring security 认证流程
认证服务
spring security 自动调用UserDetailServiceImpl(它继承了UserDetailService接口)
用户认证授权流程
用户登陆首页--首页访问认证服务--认证服务访问用户中心查询用户是否存在--如果存在,将jwt令牌(长令牌,包含用户身份信息)等所有的令牌都存放到了redis,将token(短令牌(jti),用户身份令牌)存放到cookie返回给用户
访问课程管理前端,通过前端访问用户中心--首先访问认证服务接口通过token查询jwt令牌,将令牌添加到header中--访问网关,网关通过token在redis中校验令牌是否有效--有效,请求微服务(用户中心),用户中心从header中拿到jwt令牌(每一个微服务自己都可以校验令牌合法性)