changgou-user-oauth
CREATE TABLE `oauth_client_details` (
`client_id` varchar(48) NOT NULL COMMENT '客户端ID,主要用于标识对应的应用',
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL COMMENT '客户端秘钥,BCryptPasswordEncoder加密算法加密',
`scope` varchar(256) DEFAULT NULL COMMENT '对应的范围',
`authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '认证模式',
`web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '认证后重定向地址',
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL COMMENT '令牌有效期',
`refresh_token_validity` int(11) DEFAULT NULL COMMENT '令牌刷新周期',
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
配置文件配置:
server:
port: 9001
spring:
application:
name: user-auth
redis:
host: 192.168.211.132
port: 6379
password:
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.211.132:3306/changgou_oauth?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=UTC
username: root
password: 123456
main:
allow-bean-definition-overriding: true
eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
auth:
ttl: 3600 #token存储到cookie的过期时间
clientId: changgou
clientSecret: changgou
cookieDomain: localhost
cookieMaxAge: -1
encrypt:
key-store:
location: classpath:/changgou66.jks
secret: changgou66
alias: changgou66
password: changgou66
启动之前,先启动eureka,再启动该授权认证工程
Oauth2有以下授权模式
1.授权码模式(Authorization Code)
2.隐式授权模式(Implicit)
3.密码模式(Resource Owner Password Credentials)
4.客户端模式(Client Credentials)
其中授权码和密码模式应用较多
授权码模式,流程如下:
1、客户端请求第三方授权
2、用户(资源拥有者)同意给客户端授权
3、客户端获取到授权码,请求认证服务器申请 令牌
4、认证服务器向客户端响应令牌
5、客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权
6、资源服务器返回受保护资源
请求认证服务获取授权码:
Get请求:
http://localhost:9001/oauth/authorize?client_id=changgou&response_type=code&scop=app&redirect_uri=http://localhost
参数列表如下
client_id:客户端id,和授权配置类中设置的客户端id一致。
response_type:授权码模式固定为code
scop:客户端范围,和授权配置类中设置的scop一致。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)
输入上面的url后首先跳转到登录页面:
输入账号和密码,点击Login。 Spring Security接收到请求会调用UserDetailsService
接口的loadUserByUsername
方法查询用户正确的密码。 当前导入的基础工程中客户端ID为changgou,秘钥也为changgou即可认证通过。
接下来进入授权页面:
点击Authorize,接下来返回授权码: 认证服务携带授权码跳转redirect_uri,code=PXBBQ3就是返回的授权码(每次生成的不一样)
授权码默认失效时间为60s,超过该时间需要重新授权。
拿到授权码后,申请令牌。 Post请求:http://localhost:9001/oauth/token 参数如下:
grant_type:授权类型,填写authorization_code,表示授权码模式
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
此链接需要使用 http Basic认证。 什么是http Basic认证? http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用base64编 码,放在header中请求服务端,一个例子: Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA= 是用户名:密码的base64编码。 认证失败服务端返回 401 Unauthorized。
授权后:
客户端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的唯一标识
Spring Security Oauth2提供校验令牌的端点,如下:
Get: http://localhost:9001/oauth/check_token?token= [access_token]
参数:
token:令牌
测试结果如下:
刷新令牌是当令牌快过期时重新生成一个令牌,它于授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码 也不需要账号和密码,只需要一个刷新令牌、客户端id和客户端密码。
测试如下: Post:http://localhost:9001/oauth/token
参数:
grant_type
: 固定为 refresh_token
refresh_token
:刷新令牌(注意不是access_token,而是refresh_token)
grant_type: refresh_token
refresh_token: 使用申请令牌时同时生成的刷新令牌值
密码模式
(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接 通过用户名和密码即可申请令牌。
测试如下:
Post请求:http://localhost:9001/oauth/token
参数:
grant_type:密码模式授权填写password
username:账号
password:密码
并且此链接需要使用 http Basic
认证。
Spring Security Oauth2提供校验令牌的端点,如下:
Get: http://localhost:9001/oauth/check_token?token=生成的token值
参数:
token:令牌
使用postman测试如下:
{
"scope": [
"app"
],
"name": null,
"active": true,
"id": null,
"exp": 1574324992,
"authorities": [
"seckill_list",
"goods_list"
],
"jti": "6e4bd84e-e2cd-492f-9c70-2788aa804e6e",
"client_id": "changgou",
"username": "szitheima"
}
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基础上扩展的用户身份信息
(1)传统授权流程
资源服务器授权流程,客户端先去授权服务器申请令牌,申请令牌后,携带令牌访问资源服务器,资源服务器访问授权服务校验令牌的合法性,授权服务会返回校验结果,如果校验成功会返回用户信息给资源服务器,资源服务器如果接收到的校验结果通过了,则返回资源给客户端。
传统授权方法的问题是用户每次请求资源服务,资源服务都需要携带令牌访问认证服务去校验令牌的合法性,并根 据令牌获取用户的相关信息,性能低下。
(2)公钥私钥授权流程
传统的授权模式性能低下,每次都需要请求授权服务校验令牌合法性,我们可以利用公钥私钥完成对令牌的加密,如果加密解密成功,则表示令牌合法,如果加密解密失败,则表示令牌无效不合法,合法则允许访问资源服务器的资源,解密失败,则不允许访问资源服务器资源。
上图的业务流程如下:
1、客户端请求认证服务申请令牌
2、认证服务生成令牌认证服务采用非对称加密算法,使用私钥生成令牌。
3、客户端携带令牌访问资源服务客户端在Http header 中添加: Authorization:Bearer 令牌。
4、资源服务请求认证服务校验令牌的有效性资源服务接收到令牌,使用公钥校验令牌的合法性。
5、令牌有效,资源服务向客户端响应资源信息
张三有两把钥匙,一把是公钥,另一把是私钥。
张三把公钥送给他的朋友们----李四、王五、赵六----每人一把。
李四要给张三写一封保密的信。她写完后用张三的公钥加密,就可以达到保密的效果。
张三收信后,用私钥解密,就看到了信件内容。这里要强调的是,只要张三的私钥不泄露,这封信就是安全的,即使落在别人手里,也无法解密。
张三给李四回信,决定采用"数字签名"。他写完后先用Hash函数,生成信件的摘要(digest)。张三将这个签名,附在信件下面,一起发给李四。
李四收信后,取下数字签名,用张三的公钥解密,得到信件的摘要。由此证明,这封信确实是张三发出的。李四再对信件本身使用Hash函数,将得到的结果,与上一步得到的摘要进行对比。如果两者一致,就证明这封信未被修改过。
Spring Security 提供对JWT的支持,本节我们使用Spring Security 提供的JwtHelper来创建JWT令牌,校验JWT令牌 等操作。 这里JWT令牌我们采用非对称算法进行加密,所以我们要先生成公钥和私钥。
(1)生成密钥证书 下边命令生成密钥证书,采用RSA 算法每个证书包含公钥和私钥
创建一个文件夹,在该文件夹下执行如下命令行:
keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou
keytool -genkeypair -alias changgou118 -keyalg RSA -keypass changgou118 -keystore changgou118.jks -storepass changgou118
PS:
1、通过管理员权限打开cmd
2、JAVA_HOME必须配置为系统环境变量,不能使用用户变量,否则该指令无法执行。
Keytool 是一个java提供的证书管理工具
-alias:密钥的别名
-keyalg:使用的hash算法
-keypass:密钥的访问密码
-keystore:密钥库文件名,xc.keystore保存了生成的证书
-storepass:密钥库的访问密码
(2)查询证书信息
keytool -list -keystore changgou.jks
keytool -list -keystore changgou66.jks
(3)删除别名
keytool -delete -alias changgou -keystore changgou.jsk
openssl是一个加解密工具包,这里使用openssl来导出公钥信息。
安装 openssl:http://slproweb.com/products/Win32OpenSSL.html
安装资料目录下的Win64OpenSSL-1_1_0g.exe
配置openssl的path环境变量,如下图:
cmd进入changgou.jks文件所在目录执行如下命令(如下命令在windows下执行,会把-变成中文方式,请将它改成英文的-):
keytool ‐list ‐rfc ‐‐keystore changgou.jks | openssl x509 ‐inform pem ‐pubkey
keytool -list -rfc --keystore changgou118.jks | openssl x509 -inform pem -pubkey
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvy/YKB9ERd6E0wTFmQ0A
I4ChUoeXKAIguPg5GIbM5UIyjDoU/S58obLQzwPDD2VNoqEHJECiaxXICqe7g0ai
CaStJ0lk0sKvXCUtk3LpkSOIK8BETIYYLpfGQ7Oq1A7m8VX3Wp92+lZtH3yIADqv
ADXEWewDa6nvqzxjwZGu8zwAFmPdWh78eHWI2CX2T9D3OiZQQdfCHkrLLuR7ITXo
+fZzbdreANcDLB2pJoo41VwsRmN94wqQCTzW6zT/bcpcPyXlV0AdcY/JD3rf4Ggq
SMF5Q2d8mw2fbwWL2qcqRN/wQl+7PbhC1DFlmjWXpzyNmYe2H6FbpZ9bhsIp+/xn
RwIDAQAB
-----END PUBLIC KEY-----
将上边的公钥拷贝到文本public.key文件中,合并为一行,可以将它放到需要实现授权认证的工程中(例如:changgou-service-user工程)。
将changgou.jks文件放到认证授权的工程中(changgou-user-oauth工程中)
(1)创建令牌数据
在changgou-user-oauth工程中创建测试类com.changgou.token.CreateJwtTest,使用它来创建令牌信息,代码如下:
public class CreateJwtTest {
/***
* 创建令牌测试
*/
@Test
public void testCreateToken(){
//证书文件路径
String key_location="changgou.jks";
//秘钥库密码
String key_password="changgou";
//秘钥密码
String keypwd = "changgou";
//秘钥别名
String alias = "changgou";
//访问证书路径
ClassPathResource resource = new ClassPathResource(key_location);
//创建秘钥工厂
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource,key_password.toCharArray());
//读取秘钥对(公钥、私钥)
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypwd.toCharArray());
//获取私钥
RSAPrivateKey rsaPrivate = (RSAPrivateKey) keyPair.getPrivate();
//定义Payload
Map<String, Object> tokenMap = new HashMap<>();
tokenMap.put("id", "1");
tokenMap.put("name", "itheima");
tokenMap.put("roles", "ROLE_VIP,ROLE_USER");
//生成Jwt令牌
Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(rsaPrivate));
//取出令牌
String encoded = jwt.getEncoded();
System.out.println(encoded);
}
}
运行后的结果如下:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJpdGhlaW1hIiwiaWQiOiIxIn0.RySqmgA_LBrOy5eNzQ4oXF4ZA5lZjBf2WQ37Q-KTC3A9ShiwORC5lbVBsxwx4spZg-UcUJI9JxXJwT6sga_itN3En4CnkQGklcdurriVpT96w9wk-sybgqmoAbw60TDYWvS8CyX7m1yXxBoVvtYgnkU6lwXRnLpEdrmeOkZOeW2KFj0OgN3ZWiRqWxQsKb5eGAJPTuNfvdk5rpoHGxwSLtyjBu8mIvIGVk9S1B466E9i1fd5puUSJiqP9Ic2bEAQfQuRXVhs3hN7Ikbvb-chU3ivYPoQTL8qL8v_0adkULBD8oLNDWR2jn7ZpBoDSRzcNz5PGPB8iqjsADX17Her5g
(2)解析令牌
上面创建令牌后,我们可以对JWT令牌进行解析,这里解析需要用到公钥,我们可以将之前生成的公钥public.key拷贝出来用字符串变量token存储,然后通过公钥解密。
在changgou-user-oauth创建测试类com.changgou.token.ParseJwtTest实现解析校验令牌数据,代码如下:
public class ParseJwtTest {
/***
* 校验令牌
*/
@Test
public void testParseToken(){
//令牌
String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJpdGhlaW1hIiwiaWQiOiIxIn0.IR9Qu9ZqYZ2gU2qgAziyT38UhEeL4Oi69ko-dzC_P9-Vjz40hwZDqxl8wZ-W2WAw1eWGIHV1EYDjg0-eilogJZ5UikyWw1bewXCpvlM-ZRtYQQqHFTlfDiVcFetyTayaskwa-x_BVS4pTWAskiaIKbKR4KcME2E5o1rEek-3YPkqAiZ6WP1UOmpaCJDaaFSdninqG0gzSCuGvLuG40x0Ngpfk7mPOecsIi5cbJElpdYUsCr9oXc53ROyfvYpHjzV7c2D5eIZu3leUPXRvvVAPJFEcSBiisxUSEeiGpmuQhaFZd1g-yJ1WQrixFvehMeLX2XU6W1nlL5ARTpQf_Jjiw";
//公钥
String publickey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAmt47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnhcP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEmoLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZSxtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv9QIDAQAB-----END PUBLIC KEY-----";
//校验Jwt
Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));
//获取Jwt原始内容
String claims = jwt.getClaims();
System.out.println(claims);
//jwt令牌
String encoded = jwt.getEncoded();
System.out.println(encoded);
}
}
运行后的结果如下:
添加依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
将公钥放入changgou-user-service工程的配置文件夹下
/**
* @author 栗子
* @Description
* @Date 19:28 2019/8/21
* 1:获取公钥
* 2:将公钥作为秘钥(RSA[非对称加密])
* 3:令牌的校验
* @return
**/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)// 激活方法上的PreAuthorize注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
//公钥
private static final String PUBLIC_KEY = "public.key";
/***
* 定义JwtTokenStore
* @param jwtAccessTokenConverter
* @return
*/
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
/***
* 定义JJwtAccessTokenConverter
* @return
*/
@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("\n"));
} catch (IOException ioe) {
return null;
}
}
/***
* SpringSecurity
* Http安全配置,对每个到达系统的http请求链接进行校验
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
// 所有请求必须认证通过
http.authorizeRequests()
// 下边的路径放行
.antMatchers(
"/user/add"). // 配置地址放行
permitAll()
.anyRequest().
authenticated(); // 其他地址需要认证授权
}
}
修改授权中心配置
encrypt:
key-store:
location: classpath:/changgou.jks
secret: changgou
alias: changgou
password: changgou
测试:
Authorization bearer token值(bearer后有空格)
1、用户登录,请求认证服务
2、认证服务认证通过,生成jwt令牌,将jwt令牌及相关信息写入cookie
3、用户访问资源页面,带着cookie到网关
4、网关从cookie获取token,如果存在token,则校验token合法性,如果不合法则拒绝访问,否则放行
5、用户退出,请求认证服务,删除cookie中的token
认证服务要实现的功能:
1.登录接口
用户登录时提交账号密码,校验通过,生成token令牌并写入cookie中
2.退出接口
用户退出时,通过认证模块确认删除cookie中的token令牌
在changgou-user-oauth工程中添加如下工具对象,方便操作令牌信息。
创建com.changgou.oauth.util.AuthToken类,存储用户令牌数据,代码如下
public class AuthToken implements Serializable{
//令牌信息
String accessToken;
//刷新token(refresh_token)
String refreshToken;
//jwt短令牌
String jti;
//...get...set
}
创建com.changgou.oauth.util.CookieUtil类,操作Cookie,代码如下:
public class CookieUtil {
/**
* 设置cookie
*
* @param response
* @param name cookie名字
* @param value cookie值
* @param maxAge cookie生命周期 以秒为单位
*/
public static void addCookie(HttpServletResponse response, String domain, String path, String name,
String value, int maxAge, boolean httpOnly) {
Cookie cookie = new Cookie(name, value);
cookie.setDomain(domain);
cookie.setPath(path);
cookie.setMaxAge(maxAge);
cookie.setHttpOnly(httpOnly);
response.addCookie(cookie);
}
/**
* 根据cookie名称读取cookie
* @param request
* @return map
*/
public static Map<String,String> readCookie(HttpServletRequest request, String ... cookieNames) {
Map<String,String> cookieMap = new HashMap<String,String>();
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
String cookieName = cookie.getName();
String cookieValue = cookie.getValue();
for(int i=0;i<cookieNames.length;i++){
if(cookieNames[i].equals(cookieName)){
cookieMap.put(cookieName,cookieValue);
}
}
}
}
return cookieMap;
}
}
创建com.changgou.oauth.util.UserJwt类,封装SpringSecurity中User信息以及用户自身基本信息,代码如下:
public class UserJwt extends User {
private String id; //用户ID
private String name; //用户名字
public UserJwt(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
//...get...set
}
我们现在实现一个认证流程,用户从页面输入账号密码,到认证服务的Controller层,Controller层调用Service层,Service层调用OAuth2.0的认证地址,进行密码授权认证操作,如果账号密码正确了,就返回令牌信息给Service层,Service将令牌信息给Controller层,Controller层将数据存入到Cookie中,再响应用户。
创建com.changgou.oauth.service.AuthService接口,并添加授权认证方法:
public interface AuthService {
/***
* 授权认证方法
*/
AuthToken login(String username, String password, String clientId, String clientSecret);
}
创建com.changgou.oauth.service.impl.AuthServiceImpl实现类,实现获取令牌数据,这里认证获取令牌采用的是密码授权模式,用的是RestTemplate向OAuth服务发起认证请求,代码如下:
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private RestTemplate restTemplate;
/***
* 授权认证方法
* @param username
* @param password
* @param clientId
* @param clientSecret
* @return
*/
@Override
public AuthToken login(String username, String password, String clientId, String clientSecret) {
//申请令牌
AuthToken authToken = applyToken(username,password,clientId, clientSecret);
if(authToken == null){
throw new RuntimeException("申请令牌失败");
}
return authToken;
}
/****
* 认证方法
* @param username:用户登录名字
* @param password:用户密码
* @param clientId:配置文件中的客户端ID
* @param clientSecret:配置文件中的秘钥
* @return
*/
private AuthToken applyToken(String username, String password, String clientId, String clientSecret) {
//选中认证服务的地址
ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
if (serviceInstance == null) {
throw new RuntimeException("找不到对应的服务");
}
//获取令牌的url
String path = serviceInstance.getUri().toString() + "/oauth/token";
//定义body
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
//授权方式
formData.add("grant_type", "password");
//账号
formData.add("username", username);
//密码
formData.add("password", password);
//定义头
MultiValueMap<String, String> header = new LinkedMultiValueMap<>();
header.add("Authorization", httpbasic(clientId, clientSecret));
//指定 restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
//当响应的值为400或401时候也要正常响应,不要抛出异常
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
Map map = null;
try {
//http请求spring security的申请令牌接口
ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(path, HttpMethod.POST,new HttpEntity<MultiValueMap<String, String>>(formData, header), Map.class);
//获取响应数据
map = mapResponseEntity.getBody();
} catch (RestClientException e) {
throw new RuntimeException(e);
}
if(map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null) {
//jti是jwt令牌的唯一标识作为用户身份令牌
throw new RuntimeException("创建令牌失败!");
}
//将响应数据封装成AuthToken对象
AuthToken authToken = new AuthToken();
//访问令牌(jwt)
String accessToken = (String) map.get("access_token");
//刷新令牌(jwt)
String refreshToken = (String) map.get("refresh_token");
//jti,作为用户的身份标识
String jwtToken= (String) map.get("jti");
authToken.setJti(jwtToken);
authToken.setAccessToken(accessToken);
authToken.setRefreshToken(refreshToken);
return authToken;
}
/***
* base64编码
* @param clientId
* @param clientSecret
* @return
*/
private String httpbasic(String clientId,String clientSecret){
//将客户端id和客户端密码拼接,按“客户端id:客户端密码”
String string = clientId+":"+clientSecret;
//进行base64编码
byte[] encode = Base64Utils.encode(string.getBytes());
return "Basic "+new String(encode);
}
}
创建控制层com.changgou.oauth.controller.AuthController,编写用户登录授权方法,代码如下:
@RestController
@RequestMapping(value = "/user")
public class AuthController {
//客户端ID
@Value("${auth.clientId}")
private String clientId;
//秘钥
@Value("${auth.clientSecret}")
private String clientSecret;
//Cookie存储的域名
@Value("${auth.cookieDomain}")
private String cookieDomain;
//Cookie生命周期
@Value("${auth.cookieMaxAge}")
private int cookieMaxAge;
@Autowired
AuthService authService;
@PostMapping("/login")
public Result login(String username, String password) {
if(StringUtils.isEmpty(username)){
throw new RuntimeException("用户名不允许为空");
}
if(StringUtils.isEmpty(password)){
throw new RuntimeException("密码不允许为空");
}
//申请令牌
AuthToken authToken = authService.login(username,password,clientId,clientSecret);
//用户身份令牌
String access_token = authToken.getAccessToken();
//将令牌存储到cookie
saveCookie(access_token);
return new Result(true, StatusCode.OK,"登录成功!");
}
/***
* 将令牌存储到cookie
* @param token
*/
private void saveCookie(String token){
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
CookieUtil.addCookie(response,cookieDomain,"/","Authorization",token,cookieMaxAge,false);
}
}
使用postman测试:
Post请求:http://localhost:9001/user/login
com.changgou.oauth.controller.LoginController
package com.changgou.oauth.controller;
import com.changgou.oauth.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* @author :屈雪耀
* @date :Created in 2019/11/21 20:07
* @description:
* @modified By:
* @version: $
*/
@RestController
@RequestMapping("/user")
public class LoginController {
@Value("${auth.clientId}")
private String clientId;
@Value("${auth.clientSecret}")
private String clientSecret;
@Autowired(required = false)
private LoginService loginService;
/**
* 登录验证封装令牌token
* @return
*/
@PostMapping("/login")
public Map<String,Object> login(String username,String password){
//密码授权
final String grant_type = "password";
return loginService.login(username,password,clientId,clientSecret,grant_type);
}
}
com.changgou.oauth.service.LoginService
package com.changgou.oauth.service;
import java.util.Map;
/**
* @author :屈雪耀
* @date :Created in 2019/11/21 20:06
* @description:用户登录相关
* @modified By:
* @version: $
*/
public interface LoginService {
Map<String,Object> login(String usrname, String password, String clientId, String clientSecret,String grant_type);
}
com.changgou.oauth.service.impl.LoginServiceImpl
package com.changgou.oauth.service.impl;
import com.changgou.oauth.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.io.UnsupportedEncodingException;
import java.util.Base64;
import java.util.Map;
/**
* @author :屈雪耀
* @date :Created in 2019/11/21 20:16
* @description:
* @modified By:
* @version: $
*/
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
/**
* 登录验证
* @param username 用户名
* @param password 密码
* @param clientId 客户端id
* @param clientSecret 客户端密码
* @param grant_type 授权模式
* @return
*/
@Override
public Map<String,Object> login(String username, String password, String clientId, String clientSecret,String grant_type) {
ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
String uri = serviceInstance.getUri().toString();
String url = uri + "/oauth/token";
//.getUri().toString() + "/oauth/token";
LinkedMultiValueMap headers = new LinkedMultiValueMap();
byte[] bytes = Base64.getEncoder().encode((clientId + ":" + clientSecret).getBytes());
String encode = null;
try {
encode = new String(bytes,"UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
headers.add("Authorization","Basic "+encode);
LinkedMultiValueMap body = new LinkedMultiValueMap();
body.add("username",username);
body.add("password",password);
body.add("grant_type",grant_type);
HttpEntity requestEntity = new HttpEntity(body,headers);
ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
Map map = responseEntity.getBody();
return map;
}
}
测试: