使用springboot,Oauth2.0,jwt令牌实现单点登录,权限控制等功能的基本流程
作者:互联网
本次主要是用来记录,使用springboot,Oauth2.0,jwt令牌的一些基本知识,以及利用以上几乎来实现单点登录,权限控制等功能的基本流程。
1.授权流程
本次授权流程如下图所示,采用私钥签名,公钥校验的方式来校验token令牌。
2.公钥私钥
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 是一个java提供的证书管理工具
-alias:密钥的别名
-keyalg:使用的hash算法
-keypass:密钥的访问密码
-keystore:密钥库文件名,xc.keystore保存了生成的证书
-storepass:密钥库的访问密码
(2)查询证书信息
keytool -list -keystore changgou.jks
(3)删除别名
keytool -delete -alias changgou -keystore changgou.jsk
4.2.3 导出公钥
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
下面段内容是公钥
-----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文件中,合并为一行,可以将它放到需要实现授权认证的工程中。
3.创建jwt令牌
(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.IR9Qu9ZqYZ2gU2qgAziyT38UhEeL4Oi69ko-dzC_P9-Vjz40hwZDqxl8wZ-W2WAw1eWGIHV1EYDjg0-eilogJZ5UikyWw1bewXCpvlM-ZRtYQQqHFTlfDiVcFetyTayaskwa-x_BVS4pTWAskiaIKbKR4KcME2E5o1rEek-3YPkqAiZ6WP1UOmpaCJDaaFSdninqG0gzSCuGvLuG40x0Ngpfk7mPOecsIi5cbJElpdYUsCr9oXc53ROyfvYpHjzV7c2D5eIZu3leUPXRvvVAPJFEcSBiisxUSEeiGpmuQhaFZd1g-yJ1WQrixFvehMeLX2XU6W1nlL5ARTpQf_Jjiw
(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);
}
}
运行后的结果如下:
![
在这里插入图片描述
](https://www.icode9.com/i/ll/?i=20200529054527527.png)
4.Oauth微服务开发
- controller层提供了一个认证接口/user/login
@RestController
@RequestMapping("/user")
public class AuthController {
@Autowired
AuthService authService;
@Value("${auth.clientId}")
private String clientId;
@Value("${auth.clientSecret}")
private String clientSecret;
@Value("${auth.cookieDomain}")
private String cookieDomain;
@Value("${auth.cookieMaxAge}")
private int cookieMaxAge;
@RequestMapping("/login")
public Result login(String username, String password, HttpServletResponse response){
if(StringUtils.isEmpty(username)){
throw new RuntimeException("用户名不允许为空");
}
if(StringUtils.isEmpty(password)){
throw new RuntimeException("密码不允许为空");
}
AuthToken authToken = authService.login(username, password, clientId, clientSecret);
String accessToken = authToken.getAccessToken();
CookieUtil.addCookie(response,cookieDomain,"/","Authorization",accessToken,cookieMaxAge,false);
return new Result(true, StatusCode.OK,"登录成功");
}
}
- service层实现
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
RestTemplate restTemplate;
@Autowired
LoadBalancerClient loadBalancerClient;
/**
* 密码登录模式
* @param username 用户名
* @param password 用户密码
* @param clientId 客户端id
* @param clientSecret 客户端密钥
* @return
*/
@Override
public AuthToken login(String username, String password, String clientId, String clientSecret) {
//获取请求地址 http://localhost:9001/oauth/token
//选中认证服务的地址
ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
if(serviceInstance==null){
throw new RuntimeException("找不到对应的微服务");
}
//获取令牌的url
String path = serviceInstance.getUri().toString() + "/oauth/token";
//定义body
MultiValueMap<String, String> bodyMap = new LinkedMultiValueMap<>();
bodyMap.add("grant_type","password");
bodyMap.add("username",username);
bodyMap.add("password",password);
//定义头
MultiValueMap<String, String> headerMap = new LinkedMultiValueMap<>();
//头格式为Basic +
// clientId:clientSecret的Base65转码后的结果
String string = clientId+":"+clientSecret;
byte[] encode = Base64Utils.encode(string.getBytes());
String headerStr = "Basic "+new String(encode);
headerMap.add("Authorization",headerStr);
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
//当响应的值为400或401时候也要正常响应,不要抛出异常
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
//向服务端发送请求
//http请求spring security的申请令牌接口
Map map=null;
ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(path, HttpMethod.POST,
new HttpEntity<MultiValueMap<String, String>>(bodyMap, headerMap), Map.class);
map = mapResponseEntity.getBody();
if(map==null || map.get("access_token")==null || map.get("refresh_token")==null || map.get("jti")==null){
throw new RuntimeException("创建令牌失败");
}
AuthToken authToken = new AuthToken();
authToken.setAccessToken(map.get("access_token").toString());
authToken.setRefreshToken(map.get("refresh_token").toString());
authToken.setJti(map.get("jti").toString());
return authToken;
}
- SpringSecurityOauth的一些其他配置,此处省略
5.oauth2.0微服务之间的认证
1.认证流程
1.用户在访问oauth微服务时,比如说在进行登录时,肯定会访问user微服务,调用findByUserId方法查询用户名是否存在
2.这时候就会存在一个微服务之间的认证问题,user微服务会校验oauth微服务的feign调用是否还有token,以及token的格式,权限,但是用户此时还未登录
没有token令牌
3.由于是自己系统内部的微服务互相调用,此时可以设置一个feign调用拦截器,手动生成一个token,设置权限为admin后,在进行feign调用
4.user微服务收到请求后,校验token,根据id查询对应的用户信息并返回。
5.若是访问其他微服务,如果用户已登录,直接在feign调用之前获取请求头文件,将其中的内容存到requestTemplate的头文件中去
2.详细步骤
1.登录请求
- 定义一个拦截器
在项目目录下创建interceptor包,并创建OauthInterceptor类
public class OauthInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
AdminToken adminToken = new AdminToken();
String token = "bearer "+adminToken.createAdminToken();
requestTemplate.header("Authorization",token);
}
}
2.当中的AdminToken是一个工具类,专门为访问user微服务远程调用时,因为是登录操作,还没有携带token,又需要根据用户名查询用户信息,所以手动创建一个token,存入到头文件中
代码如下:
public String createAdminToken(){
//证书文件路径
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("changgou");
//获取私钥
RSAPrivateKey rasPrivate = (RSAPrivateKey) keyPair.getPrivate();
//定义payload(载荷)
HashMap<String, Object> tokenMap = new HashMap<>();
tokenMap.put("id",1);
tokenMap.put("name","itheima");
//登录调用是内部调用,直接给与admin泉休闲
tokenMap.put("authorities",new String[]{"admin","oauth"});
//生成Jwt令牌
Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(rasPrivate));
//取出令牌
String encoded = jwt.getEncoded();
//System.out.println(encoded);
return encoded;
}
2.其他请求
拦截器内容如下:
public class FeignInterceptor implements RequestInterceptor{
/**
* 每次微服务调用之前,都先检查头文件
* 并将头文件中的令牌数据,存放到header中
* @param requestTemplate
*/
@Override
public void apply(RequestTemplate requestTemplate) {
//使用RequestContextHolder工具获取request相关变量
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//判断是否为空
if(attributes!=null){
Enumeration<String> headerNames = attributes.getRequest().getHeaderNames();
if(headerNames!=null){
while (headerNames.hasMoreElements()){
String headerName = headerNames.nextElement();
String values = attributes.getRequest().getHeader(headerName);
requestTemplate.header(headerName,values);
}
}
}
//遍历,加入到新的header中
}
}
其他操作类似
详情见本地文档
标签:令牌,springboot,jwt,token,new,Oauth2.0,public,changgou,String 来源: https://blog.csdn.net/weixin_45141938/article/details/106417473