13. SpringCloud之集成Oauth2.0权限校验之J
1、前言
前面 讲到, oauth2.0不管是 客户端模式,密码模式还是授权码模式, token由于 不承载任何 信息, 所以 token的合法性校验以及用户的权限信息 只能通过调 认证服务器 的 检查接口 才能获取到。在微服务中,所有需要权限校验的接口 都多了一次 检查token接口的 调用 ,这就对 接口的响应速度有一定影响了。
那么有什么办法 可以 省略掉 这步 向认证服务器 请求检验token,获取用户信息的接口调用呢?
答案 就是 认证服务器持有私钥, 利用 jwt 结合 非对称加密算法(如RSA) 生成 一个 承载了用户信息的 token。资源服务器 通过 与 认证服务器相互匹配的 公钥 ,进行解密 ,让资源服务器自己来校验 token的合法性 ,并且从中 解析到 用户的权限信息,从而做更细致的权限校验。
也可以采用对称加密 算法, 认证服务器和 资源服务器 持有 相同的密钥 就可以 对 token进行 加解密了。
这样就可以 不用再请求 认证服务器 校验token,获取用户信息了。
2、JWT简介
全名 json web token,是一种无状态的权限认证方式,一般用于前后端分离,时效性比较短的权限校验。
Jwt 的 token 信息分成三个部分,用“.”号分割的。
- 第一部分:头信息,通过 base64 加密生成 ,包含加密的算法, token的类型。
- 第二部分:有效载荷(用户信息),通过 base64 加密生成 。
- 第三部分:签名,根据头信息中的加密算法通过,RSA(base64(头信息) + “.” + base64(有效载 荷))生成的第三部分内容。
jwt官网(jwt.io)示例:
3、SpringCloud集成outh2.0使用Jwt鉴权
除了对jwt配置的一些信息意外,其他大部分代码配置 都和之前的密码模式 差不多。
3.1.生成公钥 和 私钥
3.1.1、私钥
cd 到 jdk 的 bin 目录执行该指令
# oauth-jwt-server 是私钥文件名称
keytool -genkeypair -alias oauth-jwt-server
-validity 3650
-keyalg RSA
-dname "CN=jwt,OU=jtw,O=jwt,L=zurich,S=zurich, C=CH"
-keypass 123456
-keystore oauth-jwt-server.jks
-storepass 123456
会在 bin 目录下生成 oauth-jwt-server.jks 文件
image.png把这个文件放到认证服务器的resouces目录下
一会 用代码 定义jwt 转换器的时候 需要加载它。
image.png3.1.2、公钥
针对 私钥文件生成 与之配置的公钥
oauth-jwt-server.jks 是刚刚生成的 私钥文件
keytool -list -rfc --keystore oauth-jwt-server.jks | openssl x509 -inform pem -pubkey
会返回 一串 公钥
image.png在资源服务器的resouces目录下 新建一个public.cert,文件内容就是上面的这串公钥。
image.png3.2、认证服务器代码配置
3.2.1、认证服务器配置
认证服务器配置里 和 密码模式不一样的 只有 配置token的部分, 其他都一模一样。
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Autowired
private UserServiceDetail userServiceDetail;
@Autowired
private DataSource dataSource;
@Bean // 声明 ClientDetails实现
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 配置token的存储方式为JwtTokenStore
endpoints.tokenStore(tokenStore())
// 配置用于JWT私钥加密的增强器
.tokenEnhancer(jwtTokenEnhancer())
// 配置安全认证管理
.authenticationManager(authenticationManager)
.userDetailsService(userServiceDetail);
}
// token存储采用 JwtTokenStore
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtTokenEnhancer());
}
@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
// 配置jks文件
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("oauth-jwt-server.jks"), "123456".toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("oauth-jwt-server"));
return converter;
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
// 对获取Token的请求不再拦截
oauthServer.tokenKeyAccess("permitAll()")
// 验证获取Token的验证信息
.checkTokenAccess("isAuthenticated()");
}
}
3.2.2、其他的配置 (和密码模式一样)
UserServiceDetail类, 安全配置,pom依赖 ,配置文件 相较于之前的密码模式,都没有任何变化
1、UserServiceDetail(和密码模式一样)
还是用jpa从 数据里 查询出用户权限信息
@Service
public class UserServiceDetail implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username);
}
}
2、安全配置(和密码模式一样)
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceDetail userServiceDetail;
@Override
public @Bean
AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/*
* access(String) 如果给定的SpEL表达式计算结果为true,就允许访问
anonymous() 允许匿名用户访问
authenticated() 允许认证的用户进行访问
denyAll() 无条件拒绝所有访问
fullyAuthenticated() 如果用户是完整认证的话(不是通过Remember-me功能认证的),就允许访问
hasAuthority(String) 如果用户具备给定权限的话就允许访问
hasAnyAuthority(String…)如果用户具备给定权限中的某一个的话,就允许访问
hasRole(String) 如果用户具备给定角色(用户组)的话,就允许访问/
hasAnyRole(String…) 如果用户具有给定角色(用户组)中的一个的话,允许访问.
hasIpAddress(String 如果请求来自给定ip地址的话,就允许访问.
not() 对其他访问结果求反.
permitAll() 无条件允许访问
rememberMe() 如果用户是通过Remember-me功能认证的,就允许访问
*
* */
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();//关闭CSRF
// .exceptionHandling()
// .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
// .and()
// .authorizeRequests()
// .antMatchers("/oauth/**").permitAll()
//// .antMatchers("/**").authenticated()
// .and()
// .httpBasic();
http.requestMatchers().anyRequest()
.and()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll();
}
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public static NoOpPasswordEncoder noOpPasswordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceDetail).passwordEncoder(passwordEncoder());
}
}
3、jwt相关pom(和密码模式一样)
<!--oauth 2.0相关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-data</artifactId>
</dependency>
<!-- 持久化 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
4、配置文件(和密码模式一样)
# eureka客户端配置
spring.application.name=oauth-jwt-server
#spring.cloud.controller.uri= http://localhost:9009/
server.port=9062
#eureka.client.service-url.defaultZone=http://localhost:9001/eureka/
eureka.client.serviceUrl.defaultZone=http://admin:admin@localhost:9001/eureka/
management.endpoints.web.exposure.include=*
# 数据源
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://maomaoyu.xyz:3306/oauth2.0?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
logging.level.org.springframework.security=debug
# jpa配置
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
# 开启basic 认证
spring.security.basic.enabled=true
3.2.3、请求获取jwt token
1、请求头Authorization (和密码模式一样)
在请求头Authorization 设置 Basic 加密后的客户端 名称和密码
image.png可以看到 headers里面已经生成了Authorization请求头了
image.png2、body参数,表单提交 (和密码模式一样)
输入鉴权类型grant_type 和 用户名密码,scope
image.png3、成功获取token
image.png可以看到,jwt生成的token 由于 含有的信息比较多,相较于 oauth2.0 其他方式的token会长的多。
把这串放到jwt官网,可以成功解析中 里面的信息
image.png最终是否校验成功,要看 输入的公钥 是都匹配, 匹配的话 会显示 签名校验成功
image.png不匹配的话 (在填入的公钥后面乱加了点东西),会显示签名校验失败。
image.png
3.3、资源服务器配置
3.3.1、资源服务器配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Value("${spring.application.name}")
private String applicationName;
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/**").authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
// resources.authenticationEntryPoint(new RefreshTokenAuthenticationEntryPoint());
// 把sping实例名称作为当前资源服务器 resourceId
resources.tokenStore(tokenStore).resourceId(applicationName);
}
}
3.3.2、jwt配置
配置token存储方式为jwt,并配置 jwt转换器,加载 公钥 ,用于解密jwt token
@Configuration
public class JwtConfig {
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Bean
@Qualifier("tokenStore")
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Bean
public JwtAccessTokenConverter jwtTokenEnhancer() {
// 用作JWT转换器
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;
}
}
3.3.3、启动 方法级别鉴权(和密码模式一样)
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class GlobalMethodSecurityConfig {
}
3.3.4、配置文件
配置认证服务器 的 接口(这个接口是Oauth2.0框架自带的), 资源服务器启动的时候会 调一次这个接口 看是否正常
security.oauth2.resource.jwt.key-uri=http://localhost:9062/oauth/token_key
#由于token的合法性和token里面的用户信息 由资源服务器自己利用 公钥解析,不要再配置这个去请求认证服务器了。
#security.oauth2.resource.user-info-uri=http://127.0.0.1:9052/security/check
3.3.5、其他配置(和密码模式一样)
pom依赖 和 oauth2.0其他模式没有任何差别 ,其他的配置也没变化
1、pom依赖(和密码模式一样)
<!--oauth 2.0相关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-data</artifactId>
</dependency>
<!-- 持久化 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
2、配置文件(和密码模式一样)
spring.application.name=oauth-jwt-server
#spring.cloud.controller.uri= http://localhost:9009/
server.port=9062
#eureka.client.service-url.defaultZone=http://localhost:9001/eureka/
eureka.client.serviceUrl.defaultZone=http://admin:admin@localhost:9001/eureka/
spring.redis.database=1
spring.redis.host=maomaoyu.xyz
spring.redis.port=6379
spring.redis.password=123456
management.endpoints.web.exposure.include=*
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://maomaoyu.xyz:3306/oauth2.0?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
logging.level.org.springframework.security=debug
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.security.basic.enabled=true
3.3.6、使用token
把token放到Authorization请求头里, 内容为 Bearer+ 空格 + jwt的token,
image.png最终可以看到,请求成功
image.png4、jwt的弊端
- 由于jwt是无状态的(不在后台任何地方存储), 如果token被人盗取的话(比如抓包,浏览器F12),那么 将无法 阻止这个token 被非法使用(像有状态的token 放在redis或者数据库里,可以通过删除 这个token 来让其无法访问)。 唯一的办法 只能用https, 让token 没法被别人抓取。
- token太长,相较于 oauth2.0默认的token,放在请求头里,占用的网络带宽 会 增大。
- 无法续期,jwt 的token生成出来过期时间就已经 加密在 token 中了,没法像redis存储的session或者token一样,通过更新 过期时间来延长session/ token的有效期。到期了就只能通过refresh_token 或者 再次登录 重新生成token。