1.使用场景:
在目前大多数第三方登录的都是使用oauth2的授权码模式实现登录验证,避免用户再次注册登录。
授权码模式的认证过程主要是:
1、用户客户端请求认证服务器的认证接口,并附上回调地址;
2、认证服务接口接收到认证请求后调整到自身的登录界面;
3、用户输入用户名和密码,点击确认,跳转到授权、拒绝提示页面(也可省略);
4、用户点击授权或者默认授权后,跳转到微服务客户端的回调地址,并传入参数 code;
5、回调地址一般是一个 RESTful 接口,此接口拿到 code 参数后,再次请求认证服务器的 token 获取接口,用来换取 access_token 等信息;
6、获取到 access_token 后,拿着 token 去请求各个微服务客户端的接口。
注意上面所说的用户客户端可以理解为浏览器、app 端,微服务客户端就是我们系统中的例如订单服务、用户服务等微服务,认证服务端就是用来做认证授权的服务,相对于认证服务端来说,各个业务微服务也可以称作是它的客户端。
2.代码实现
在#yyds干货盘点# Spring cloud Oauth2增强JWT实现登录验证授权基础代码上进行修改
2.1 oauth2-service修改
修改配置
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 允许匿名访问所有接口 主要是 oauth 接口
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.and()
.authorizeRequests()
.antMatchers("/**").permitAll();
}
}
2.2 oauth2-client修改
1.添加依赖,引入 okhttp 和 thymeleaf 是因为要做一个简单的页面并模拟正常的认证过程。
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.14.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2.修改yml, 添加了redirect-url属性,用于请求接口时调用
spring:
application:
name: oauth2-client
redis:
database: 2
host: localhost
port: 6379
password: 123456
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
timeout: 100ms
server:
port: 8002
servlet:
context-path: /oauth2-client
security:
oauth2:
client:
client-id: oauth2-client
client-secret: oauth2-secret-8888
user-authorization-uri: http://localhost:8001/oauth/authorize
access-token-uri: http://localhost:8001/oauth/token
resource:
jwt:
key-uri: http://localhost:8001/oauth/token_key
key-value: dev
authorization:
check-token-access: http://localhost:8001/oauth/check_token
redirect-url: http://localhost:8002/oauth2-client/login
3.修改配置
使用 jwt 作为 token 的存储,注意允许/login 接口无授权访问,这个地址是认证的回调地址,会返回 code 参数,code用于完成授权验证操作。
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setSigningKey("dev");
accessTokenConverter.setVerifierKey("dev");
return accessTokenConverter;
}
@Autowired
private TokenStore jwtTokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(jwtTokenStore);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/login").permitAll();
}
}
4.控制层请求完成登录授权验证
@Slf4j
@Controller
public class CodeClientController {
@Value("${security.oauth2.client.access-token-uri}")
private String tokenUrl;
@Value("${security.oauth2.redirect-url}")
private String redirectUrl;
@Value("${security.oauth2.client.client-id}")
private String clientId;
@Value("${security.oauth2.client.client-secret}")
private String clientSecret;
@Autowired
private Base64Util base64Util;
/**
* 用来展示index.html 模板
* @return
*/
@GetMapping(value = "index")
public String index(){
return "index";
}
@GetMapping(value = "login")
public Object login(String code,Model model) {
OkHttpClient httpClient = new OkHttpClient();
RequestBody body = new FormBody.Builder()
.add("grant_type", "authorization_code")
.add("client", clientId)
.add("redirect_uri",redirectUrl)
.add("code", code)
.build();
String str = clientId + ":" + clientSecret;
String authorization = base64Util.encode(str);
Request request = new Request.Builder()
.url(tokenUrl)
.post(body)
.addHeader("Authorization", "Basic " + authorization)
.build();
try {
Response response = httpClient.newCall(request).execute();
String result = response.body().string();
ObjectMapper objectMapper = new ObjectMapper();
Map tokenMap = objectMapper.readValue(result,Map.class);
String accessToken = tokenMap.get("access_token").toString();
Claims claims = Jwts.parser()
.setSigningKey("dev".getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(accessToken)
.getBody();
String userName = claims.get("user_name").toString();
model.addAttribute("username", userName);
model.addAttribute("accessToken", result);
return "index";
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
5.base64加解密工具类
@Component
public class Base64Util {
public static void main(String[] args) throws Exception {
String str = "oauth2-client:oauth2-secret-8888";
base64(str);
enAndDeCode(str);
}
public String encode(String str) {
byte[] bytes = str.getBytes();
//Base64 加密
String encoded = Base64.getEncoder().encodeToString(bytes);
System.out.println("Base 64 加密后:" + encoded);
return encoded;
}
/**
* Base64
*
*/
public static void base64(String str) {
byte[] bytes = str.getBytes();
//Base64 加密
String encoded = Base64.getEncoder().encodeToString(bytes);
System.out.println("Base 64 加密后:" + encoded);
//Base64 解密
byte[] decoded = Base64.getDecoder().decode(encoded);
String decodeStr = new String(decoded);
System.out.println("Base 64 解密后:" + decodeStr);
System.out.println();
}
/**
* BASE64加密解密
*/
public static void enAndDeCode(String str) throws Exception {
String data = encryptBASE64(str.getBytes());
System.out.println("sun.misc.BASE64 加密后:" + data);
byte[] byteArray = decryptBASE64(data);
System.out.println("sun.misc.BASE64 解密后:" + new String(byteArray));
}
/**
* BASE64解密
* @throws Exception
*/
public static byte[] decryptBASE64(String key) throws Exception {
return (new BASE64Decoder()).decodeBuffer(key);
}
/**
* BASE64加密
*/
public static String encryptBASE64(byte[] key) throws Exception {
return (new BASE64Encoder()).encodeBuffer(key);
}
}
6.授权完成后跳转前端页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>测试主页</title>
</head>
<body>
<div>
<a href="http://localhost:8001/oauth/authorize?client_id=oauth2-client&response_type=code&redirect_uri=http://localhost:8002/oauth2-client/login">登录</a>
<span th:text="'当前认证用户:' + ${username}"></span>
<span th:text="${accessToken}"></span>
</div>
</body>
</html>
3.数据库插入数据
INSERT INTO `spring_cloud`.`oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('oauth2-client', NULL, '$2a$10$p4oERvFiyLCvwq7t3y8E0eJsq5D1JwFqP.slVc1xB6yXy/V23KNQu', 'all', 'authorization_code,refresh_token,password', 'http://localhost:8002/oauth2-client/login', NULL, 3600, 36000, NULL, '1');
INSERT INTO `spring_cloud`.`user_info`(`id`, `username`, `password`) VALUES ('1', 'admin', '$2a$10$erpn.mfZuRrHpFxqiQrD1.u02HMoaoCJ9.2d7azW42pmt/w7b3Fti');
4.运行oauth2-service, oauth2-client项目后,打开浏览器,输入地址
由于是配置读取数据库用户信息的,插入了用户信息为admin, 密码123456,输入后登录进入页面。
来到授权确认页面,页面上有 Authorize 和 Deny (授权和拒绝)两个按钮。可通过将 autoapprove 字段设置为 0 来取消此页面的展示,默认直接同意授权。点击同意授权后,跳转到了回调地址,完成了登录验证授权。