通过 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);
}

测试

  1. 启动 eureka-server
  2. 启动 oauth2-server
  3. 启动 config-server
  4. 启动 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的更多相关文章

  1. Spring Cloud 学习 之 Spring Cloud Eureka(源码分析)

    Spring Cloud 学习 之 Spring Cloud Eureka(源码分析) Spring Boot版本:2.1.4.RELEASE Spring Cloud版本:Greenwich.SR1 ...

  2. Spring Cloud学习笔记--Spring Boot初次搭建

    1. Spring Boot简介 初次接触Spring的时候,我感觉这是一个很难接触的框架,因为其庞杂的配置文件,我最不喜欢的就是xml文件,这种文件的可读性很不好.所以很久以来我的Spring学习都 ...

  3. Spring Cloud 学习 之 Spring Cloud Eureka(搭建)

    Spring Boot版本:2.1.4.RELEASE Spring Cloud版本:Greenwich.SR1 文章目录 搭建服务注册中心: 注册服务提供者: 高可用注册中心: 搭建服务注册中心: ...

  4. Spring Cloud 学习 (九) Spring Security, OAuth2

    Spring Security Spring Security 是 Spring Resource 社区的一个安全组件.在安全方面,有两个主要的领域,一是"认证",即你是谁:二是& ...

  5. Spring Cloud 学习 之 Spring Cloud Ribbon(基础知识铺垫)

    文章目录 1.负载均衡: 2.RestTemplate详解: xxxForEntity/xxxForObject:主要介绍get跟post exchange: execute源码分析: 1.负载均衡: ...

  6. spring cloud学习(七)Spring Cloud Config(续)

    Spring Cloud Config(续) 个人参考项目 个人博客 : https://zggdczfr.cn/ 个人参考项目 : (整合到上一个案例中)https://github.com/Fun ...

  7. spring cloud学习(六)Spring Cloud Config

    Spring Cloud Config 参考个人项目 参考个人项目 : (希望大家能给个star~) https://github.com/FunriLy/springcloud-study/tree ...

  8. Spring Cloud 学习 之 Spring Cloud Bus实现修改远程仓库后配置自动刷新

    ​ 版本号: ​ Spring Boot:2.1.3.RELEASE ​ Spring Cloud:G版 ​ 开发工具:IDEA 搭建配置中心,这里我们搭建一个简单版的就行 POM: <?xml ...

  9. Spring Cloud学习 之 Spring Cloud Ribbon 重试机制及超时设置不生效

    今天测了一下Ribbon的重试跟超时机制,发现进行的全局超时配置一直不生效,配置如下: ribbon: #单位ms,请求连接的超时时间,默认1000 ConnectTimeout: 500 #单位ms ...

随机推荐

  1. Windows 10 启动出现蓝屏 终止代码:UNMOUNTABLE_BOOT_VOLUME

    解决办法:在命令符窗口中[管理员权限] 1.– 修复Windows文件:损坏的Windows文件可能会导致严重的问题. sfc /scannow 2 .– 修复硬盘:确保您的硬盘依次运行,以及Wind ...

  2. Java进阶专题(十六) 数据结构与算法的应用(上)

    前言 ​ 学习算法,我们不需要死记硬背那些冗长复杂的背景知识.底层原理.指令语法--需要做的是领悟算法思想.理解算法对内存空间和性能的影响,以及开动脑筋去寻求解决问题的最佳方案.相比编程领域的其他技术 ...

  3. Activit的心路历程:获取当前节点的上一节点【可能存在多个】的nodeId

    在我的开发任务中,突然给我提出了一个待办任务需要获取当前任务节点上以任务节点的表单信息,刚开始搞得我有点措手不及,后来仔细是靠后,灵感一下,直接操作流程的bpmn信息就可以获取到节点信息嘛,顺着这个思 ...

  4. C# 时间格式处理

    C#的常用时间格式意义: 1字符"y"---year,年,yy显示13,yyyy显示2013 2字符"M"---Month,月份,M显示5,MM显示05 3字符 ...

  5. hadoop之yarn(优化篇)

    最近一直在学习hadoop的一些原理和优化,然后也做了一些实践,也有没有去做实践的,反正个人观点都记录下来 一.yarn的介绍 YARN的基本结构由一个ResourceManager与多个NodeMa ...

  6. js数组方法(管饱)

    有一些数组方法是ECMAScript新增的,一定要注意浏览器的兼容!! Array对象属性: 属性 说明 constructor 返回对创建此对象的函数引用 length 返回集合内的元素的所有长度 ...

  7. [转自王垠]完全用GNU/Linux工作,摈弃Windows低效率的工作方式

    ZT (a qinghua student's article) 我已经半年没有使用 Windows 的方式工作了.Linux 高效的完成了我所有的工作. GNU/Linux 不是每个人都想用的.如果 ...

  8. CSS3之transition属性

    transition属性可直译为"过渡",主要用于检索或设置对象变换的过渡. 语法: transition:property duration [timing-function] ...

  9. full nat

    在餐馆吃饭时,连接无线网络后访问某网页会自动弹出一个认证页面,我想大家都经历过..... 其网络拓扑如下: sta-------------网络设备--------------公网 比如sta 终端i ...

  10. nginx&http 第三章 ngx http ngx_http_process_request_headers

    HTTP 请求行正确处理完成后,针对 HTTP/1.0 及以上版本紧接着要做的就是请求 HEADER 的处理与解析了 /** * 用于处理http的header数据 * 请求头: * Host: lo ...