Spring Cloud 学习 (十) Spring Security, OAuth2, JWT
通过 Spring Security + OAuth2 认证和鉴权,每次请求都需要经过 OAuth Server 验证当前 token 的合法性,并且需要查询该 token 对应的用户权限,在高并发场景下会存在性能瓶颈。使用 JWT 的方式,OAuth Server 只验证一次,用户所有信息 (包括权限) 包含在返回的 JWT 中
准备工作
生成公钥、私钥
私钥
在控制台输入命令:
keytool -genkeypair -alias spring-jwt -validity 3650 -keyalg RSA -dname "CN=Victor,OU=Karonda,O=Karonda,L=Shenzhen,S=Guangdong,C=CN" -keypass abc123 -storepass abc123 -keystore spring-jwt.jks
各个参数的含义,可以通过命令查看:
keytool -genkeypair -help
其中 DName 各个参数代表的意义见: X.500 Distinguished Names
公钥
在控制台输入命令:
keytool -list -rfc --keystore spring-jwt.jks | openssl x509 -inform pem -pubkey
会提示输入密码,密码为生成私钥命令里设置的密码
本文生成的公钥:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2HvMsVqrx60ESp30Ymx7
3Ce2h24QvG9rciDPl8+SxXRz79akmdRCB4HFhBb655aVAnQMj4SGzKcMyofOUt3o
X9tOPz3Y/B/D5viI3cNPYinyFVMawganROsM1meTFR1SPpL/kZUZqLm9pc8lpgat
LtU73ryioVe7FFndce6ZwTe24L4rK0jzseQ24FxoEQ+g0B1DCXZ4Gi9PwBpxWL6W
AG+/NEFFtOGtIJSIwCYzhGqDfyNaOt7JXYwGiWgh0npO3JVvgQVXBW9AdpT5JVSb
ScYktkqY3o0htsSueyne+FbS+OwBVaBewcswPVbEwa6dxtb0vBsp3pNiSdg7rDea
1QIDAQAB
-----END PUBLIC KEY-----
新建 public.cert 文件保存上面生成的公钥 (要包含公钥的完整信息,即 BEGIN PUBLIC KEY 和 END PUBLIC KEY 部分也要包含在文件中)
Windows 系统需要先安装 OpenSSL: 下载链接
将私钥和公钥分别拷贝到 oauth2-server 和 eureka-client 的 resources 目录下
并在 oauth2-server 和 eureka-client 的 pom 添加配置:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>cert</nonFilteredFileExtension>
<nonFilteredFileExtension>jks</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
因为密钥文件不需要编译
oauth2-server
修改 Authorization Server 配置
@Configuration
@EnableAuthorizationServer // 开启授权服务
@EnableResourceServer // 需要对外暴露获取和验证 Token 的接口,所以也是一个资源服务
public class OAuth2Config extends AuthorizationServerConfigurerAdapter{
@Autowired
private AuthenticationManager authenticationManager;
@Override
// 配置客户端信息
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 将客户端的信息存储在内存中
.withClient("eureka-client") // 客户端
.secret("123456") // 客户端密码
.authorizedGrantTypes("client_credentials", "refresh_token", "password")
.accessTokenValiditySeconds(3600) // 设置 token 过期时间
.scopes("server");
}
@Override
// 配置授权 token 的节点和 token 服务
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()) // token 的存储方式
.authenticationManager(authenticationManager) // 开启密码验证,来源于 WebSecurityConfigurerAdapter
// .userDetailsService(userServiceDetail); // 读取验证用户的信息
.tokenEnhancer(jwtTokenEnhancer());
}
@Bean
public TokenStore tokenStore() {
// return new InMemoryTokenStore();
// return new JdbcTokenStore(dataSource);
return new JwtTokenStore(jwtTokenEnhancer());
}
@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer(){
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("spring-jwt.jks")
, "abc123".toCharArray()); // abc123 为 password
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("spring-jwt")); // spring-jwt 为 alias
return converter;
}
}
eureka-client
上一篇文章中的 OAuth2 Client 配置本文用不到,要移除
Resource Server 配置
配置 JWT 转换器
@Configuration
public class JwtConfig {
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer(){
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);
return converter;
}
}
修改 Resource Server 配置
@Configuration
@EnableResourceServer // 开启资源服务
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别上的保护
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
@Autowired
TokenStore tokenStore;
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/login", "/user/register").permitAll()
.anyRequest().authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore);
}
}
JWT 类
public class JWT {
private String access_token;
private String token_type;
private String refresh_token;
private int expires_in;
private String scope;
private String jti;
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public String getToken_type() {
return token_type;
}
public void setToken_type(String token_type) {
this.token_type = token_type;
}
public String getRefresh_token() {
return refresh_token;
}
public void setRefresh_token(String refresh_token) {
this.refresh_token = refresh_token;
}
public int getExpires_in() {
return expires_in;
}
public void setExpires_in(int expires_in) {
this.expires_in = expires_in;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public String getJti() {
return jti;
}
public void setJti(String jti) {
this.jti = jti;
}
@Override
public String toString() {
return "JWT{" +
"access_token='" + access_token + '\'' +
", token_type='" + token_type + '\'' +
", refresh_token='" + refresh_token + '\'' +
", expires_in=" + expires_in +
", scope='" + scope + '\'' +
", jti='" + jti + '\'' +
'}';
}
}
Feign 客户端
@FeignClient("oauth2-server")
public interface AuthServiceClient {
@PostMapping("/uaa/oauth/token")
JWT getToken(@RequestHeader(value = "Authorization") String authorization, @RequestParam("grant_type") String type,
@RequestParam("username") String username, @RequestParam("password") String password);
}
同时需要在启动类添加注解:
@EnableFeignClients
DTO 及异常处理
public class UserLoginDTO {
private JWT jwt;
private User user;
public JWT getJwt() {
return jwt;
}
public void setJwt(JWT jwt) {
this.jwt = jwt;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
public class UserLoginException extends RuntimeException {
public UserLoginException(String message){
super(message);
}
}
@ControllerAdvice // 表明该类是异常统一处理类
@ResponseBody
public class ExceptionHandle {
@ExceptionHandler(UserLoginException.class)
public ResponseEntity<String> handleException(Exception e){
return new ResponseEntity(e.getMessage(), HttpStatus.OK);
}
}
service & controller
添加登录方法
@Override
public UserLoginDTO login(String username, String password) {
User user = userDao.findByUsername(username);
if(null == user){
throw new UserLoginException("error username");
}
if(!password.equals(user.getPassword())){
throw new UserLoginException("erro password");
}
JWT jwt = authServiceClient.getToken("Basic ZXVyZWthLWNsaWVudDoxMjM0NTY="
, "password", username, password); // ZXVyZWthLWNsaWVudDoxMjM0NTY= 为 eureka-client:123456 Base64 加密后的值
if(null == jwt){
throw new UserLoginException("error internal");
}
UserLoginDTO userLoginDTO = new UserLoginDTO();
userLoginDTO.setJwt(jwt);
userLoginDTO.setUser(user);
return userLoginDTO;
}
@RequestMapping(value = "/login", method = RequestMethod.POST)
public UserLoginDTO login(@RequestParam("username") String username
, @RequestParam("password") String password){
return userService.login(username, password);
}
测试
- 启动 eureka-server
- 启动 oauth2-server
- 启动 config-server
- 启动 eureka-client
先取消授权:
DELETE FROM user_role WHERE user_id = 2;
使用 Postman 测试:
用户登录
| - | POST | localhost:8011/user/login |
| Body | ||
| - | username | admin |
| - | password | 123 |
{
"jwt": {
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjEyNjUyMzgsInVzZXJfbmFtZSI6ImFkbWluIiwianRpIjoiNTQxMDk3MDItNTU1Yy00NjA4LThkYzYtNzU3NWZjNGIwNGIyIiwiY2xpZW50X2lkIjoiZXVyZWthLWNsaWVudCIsInNjb3BlIjpbInNlcnZlciJdfQ.P6dtT76bFyQ6aF7-v6Vphi3ivLR0x4w739gwmBRGujaRpfDMjwQHCn5REyxEOAKdoxrVT__v73qcb78_8Ovb97L13ztnzdlPmLYzcAkQdMFz78yAjZIp2VtzxZ87Ecmk9f6-bIRlBxS9A24t0y4Tp1gkPITB1vxod0FewAHCsUJQ9WqLNeW9bxzZvy5DtlJlCCY7lOIjfDxlQdXygpwznZ4rIHv-O-eOr2aqcKMLZhdtW7hHsy2JccIUm1ZdpVQfUMD7XzWFAQoZYFLc0oXyVL0nFasOr-Ne1UR1iZYI4cS-ONVLMe78erVb-zRoyTAhEb7Pkyepkwm_Xv23U2CoeA",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjM4NTM2MzgsInVzZXJfbmFtZSI6ImFkbWluIiwianRpIjoiODhjYTQ0NjktMTIyNi00ZTFkLTlhMDktZDZlMTdhOTMyYzAzIiwiY2xpZW50X2lkIjoiZXVyZWthLWNsaWVudCIsInNjb3BlIjpbInNlcnZlciJdLCJhdGkiOiI1NDEwOTcwMi01NTVjLTQ2MDgtOGRjNi03NTc1ZmM0YjA0YjIifQ.RdBDYKhZJz5DntxK_4np1B4phnalT37srjycUUmCHVZ0BB4lEWAIT5YLlY7ZaaVM2AAhbeb1WO1dhlmvtmlkd8W6lowbtMeyMYqrKcbn1tYavLwZDHKWSHGiUW1bXivngwhixCqLwK0AA8Oe-9-ohC-c6G7cRN4r6bWkc4WiadlErg6MS7N6VGdQj26SgPVmTqvVhpm5mnzGJyM66d-kxneHyRjPVli1DFyxuUl8oRCTTFuamybXmD_niWCA-isDgF7loJFV6hMjoow6-3uK9rLthMADIM4YqAp8T8eGsup_7hIICwT7qUhOdzBjwsuX8ond3iu09322LsPEoTTXlg",
"expires_in": 3599,
"scope": "server",
"jti": "54109702-555c-4608-8dc6-7575fc4b04b2"
},
"user": {
"id": 2,
"username": "admin",
"password": "123",
"authorities": [],
"enabled": true,
"credentialsNonExpired": true,
"accountNonExpired": true,
"accountNonLocked": true
}
}
访问不需要权限的接口
| - | GET | localhost:8011/hi?name=Victor |
| Headers | ||
| - | Authorization | Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjEyNjUyMzgsInVzZXJfbmFtZSI6ImFkbWluIiwianRpIjoiNTQxMDk3MDItNTU1Yy00NjA4LThkYzYtNzU3NWZjNGIwNGIyIiwiY2xpZW50X2lkIjoiZXVyZWthLWNsaWVudCIsInNjb3BlIjpbInNlcnZlciJdfQ.P6dtT76bFyQ6aF7-v6Vphi3ivLR0x4w739gwmBRGujaRpfDMjwQHCn5REyxEOAKdoxrVT__v73qcb78_8Ovb97L13ztnzdlPmLYzcAkQdMFz78yAjZIp2VtzxZ87Ecmk9f6-bIRlBxS9A24t0y4Tp1gkPITB1vxod0FewAHCsUJQ9WqLNeW9bxzZvy5DtlJlCCY7lOIjfDxlQdXygpwznZ4rIHv-O-eOr2aqcKMLZhdtW7hHsy2JccIUm1ZdpVQfUMD7XzWFAQoZYFLc0oXyVL0nFasOr-Ne1UR1iZYI4cS-ONVLMe78erVb-zRoyTAhEb7Pkyepkwm_Xv23U2CoeA |
Hello Victor, from port: 8011, version: 1.0.2
访问需要权限的接口
| - | GET | localhost:8011/hello |
| Headers | ||
| - | Authorization | Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjEyNjUyMzgsInVzZXJfbmFtZSI6ImFkbWluIiwianRpIjoiNTQxMDk3MDItNTU1Yy00NjA4LThkYzYtNzU3NWZjNGIwNGIyIiwiY2xpZW50X2lkIjoiZXVyZWthLWNsaWVudCIsInNjb3BlIjpbInNlcnZlciJdfQ.P6dtT76bFyQ6aF7-v6Vphi3ivLR0x4w739gwmBRGujaRpfDMjwQHCn5REyxEOAKdoxrVT__v73qcb78_8Ovb97L13ztnzdlPmLYzcAkQdMFz78yAjZIp2VtzxZ87Ecmk9f6-bIRlBxS9A24t0y4Tp1gkPITB1vxod0FewAHCsUJQ9WqLNeW9bxzZvy5DtlJlCCY7lOIjfDxlQdXygpwznZ4rIHv-O-eOr2aqcKMLZhdtW7hHsy2JccIUm1ZdpVQfUMD7XzWFAQoZYFLc0oXyVL0nFasOr-Ne1UR1iZYI4cS-ONVLMe78erVb-zRoyTAhEb7Pkyepkwm_Xv23U2CoeA |
{
"error": "access_denied",
"error_description": "不允许访问"
}
手动授权:
INSERT INTO user_role (user_id, role_id) VALUES (2, 2);
重新登录后再次访问接口
hello!
完整代码:GitHub
本人 C# 转 Java 的 newbie, 如有错误或不足欢迎指正,谢谢
Spring Cloud 学习 (十) Spring Security, OAuth2, JWT的更多相关文章
- Spring Cloud 学习 之 Spring Cloud Eureka(源码分析)
Spring Cloud 学习 之 Spring Cloud Eureka(源码分析) Spring Boot版本:2.1.4.RELEASE Spring Cloud版本:Greenwich.SR1 ...
- Spring Cloud学习笔记--Spring Boot初次搭建
1. Spring Boot简介 初次接触Spring的时候,我感觉这是一个很难接触的框架,因为其庞杂的配置文件,我最不喜欢的就是xml文件,这种文件的可读性很不好.所以很久以来我的Spring学习都 ...
- Spring Cloud 学习 之 Spring Cloud Eureka(搭建)
Spring Boot版本:2.1.4.RELEASE Spring Cloud版本:Greenwich.SR1 文章目录 搭建服务注册中心: 注册服务提供者: 高可用注册中心: 搭建服务注册中心: ...
- Spring Cloud 学习 (九) Spring Security, OAuth2
Spring Security Spring Security 是 Spring Resource 社区的一个安全组件.在安全方面,有两个主要的领域,一是"认证",即你是谁:二是& ...
- Spring Cloud 学习 之 Spring Cloud Ribbon(基础知识铺垫)
文章目录 1.负载均衡: 2.RestTemplate详解: xxxForEntity/xxxForObject:主要介绍get跟post exchange: execute源码分析: 1.负载均衡: ...
- spring cloud学习(七)Spring Cloud Config(续)
Spring Cloud Config(续) 个人参考项目 个人博客 : https://zggdczfr.cn/ 个人参考项目 : (整合到上一个案例中)https://github.com/Fun ...
- spring cloud学习(六)Spring Cloud Config
Spring Cloud Config 参考个人项目 参考个人项目 : (希望大家能给个star~) https://github.com/FunriLy/springcloud-study/tree ...
- Spring Cloud 学习 之 Spring Cloud Bus实现修改远程仓库后配置自动刷新
版本号: Spring Boot:2.1.3.RELEASE Spring Cloud:G版 开发工具:IDEA 搭建配置中心,这里我们搭建一个简单版的就行 POM: <?xml ...
- Spring Cloud学习 之 Spring Cloud Ribbon 重试机制及超时设置不生效
今天测了一下Ribbon的重试跟超时机制,发现进行的全局超时配置一直不生效,配置如下: ribbon: #单位ms,请求连接的超时时间,默认1000 ConnectTimeout: 500 #单位ms ...
随机推荐
- 【Luogu】 P6274 [eJOI2017]六 题解
首先,题目说了最多\(6\)个质因数. 如此小的数据范围,不是状压还是啥? 然后,我们可以发现一个性质:只要两个因数有相同的质因数(不管次数是多少),两者就不互质. 这启示我们用一个二进制数来表示一类 ...
- jackson、fastjson、kryo、protostuff等序列化工具性能对比
简介 实际项目中,我们经常需要使用序列化工具来存储和传输对象.目前用得比较多的序列化工具有:jackson.fastjson.kryo.protostuff.fst 等,本文将简单对比这几款工具序列化 ...
- Vue的生命周期--代码片段
Vue 实例有一个完整的生命周期,也就是从开始创建. 初始化数据. 编译模板. 挂载 Dom. 渲染→更新→渲染. 销毁等一系列过程,我们称这是 Vue 的生命周期.通俗说就是 Vue 实例从创建到销 ...
- EF6 Code First 博客学习记录
学习一下ef6的用法 这个学习过程时按照微软官网的流程模拟了一下 就按照下面的顺序来写吧 1.连接数据库 自动生成数据库 2.数据库迁移 3.地理位置以及同步/异步处理(空了再补) 4.完全自动迁移 ...
- Linux下端口被占用,关掉端口占用的方法
Linux下端口被占用(例如端口3000),关掉端口占用的进程的方法: 1.netstat -tln | grep 3000 2.sudo lsof -i:3000 3.sudo kill -9 进程
- ctf堆叠注入总结(持续更新)
第一种 1';USE db_name;SET @sql=CONCAT('sql contents');PREPARE stmt_name FROM @sql;EXECUTE stmt_name; 第二 ...
- acm一些小细节/技巧
以后没有终止信号的输入统一用 : while(cin>>a) { ... } "1" 不是质数, 要注意. 当需要把一个数组中的数值初始化成正无穷时,为了避免加法算术 ...
- netfilter 的扩展功能
目前内核已经有filter 功能,但是往往实际运用中需要用到一些定制的filter 功能, 所以这个时候仅仅依靠现有的不能完成,于是就出现了conntrack的扩展功能, 最直接的就是tftp he ...
- Socket 套接字的系统调用
socket 结构 /** * struct socket - general BSD socket * @state: socket state (%SS_CONNECTED, etc) * @ty ...
- JAVA注解的继承性
摘要 本文从三个方面介绍java注解的**"继承性"**: 基于元注解@Inherited,类上注解的继承性 基于类的继承,方法/属性上注解的继承性 基于接口的继承/实现,方法/属 ...