授权服务器
授权服务器中有4个端点。说明如下:
- Authorize Endpoint :授权端点,进行授权。
- Token Endpoint :令牌端点,经过授权拿到对应的Token。
- lntrospection Endpoint :校验端点,校验Token的合法性。
- Revocation Endpoint :撤销端点,撤销授权。
说明如下:
- 用户访问,此时没有Token。Oauth2RestTemplate会报错,这个报错信息会被Oauth2ClientContextFilter捕获并重定向到认证服务器。
- 认证服务器通过Authorization Endpoint进行授权,并通过AuthorizationServerTokenServices生成授权码并返回给客户端。
- 客户端拿到授权码去认证服务器通过Token Endpoint调用AuthorizationServerTokenServices生成Token并返回给客户端。
- 客户端拿到Token去资源服务器访问资源,一般会通过Oauth2AuthenticationManager调用ResourceServerTokenServices进行校验。校验通过可以获取资源。
环境搭建
(2)pom依赖
<!-- spring cloud中的oauth2依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!-- spring cloud中的security依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<!-- web模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
(3)pojo
在pojo包下自定一个实体类User,但是此类必实现UserDetails接口。如下:
package com.sec.kun.pojo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
/**
* @Author zhoukun
* @Date 2021/2/17 15:45
*/
/*
* 自定义User类,需实现UserDetails接口
*/
public class User implements UserDetails {
private String username;
private String password;
private List<GrantedAuthority> authorities;
// 构造方法
public User(String username, String password, List<GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
(4)Spring Security配置类
config包下创建SecurityConfig配置类,如下:
package com.sec.kun.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @Author zhoukun
* @Date 2021/2/17 15:55
*/
/*
* Spring Security配置类
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/**","/login/**","/logout/**").permitAll()//放行
.anyRequest().authenticated()//其他路径拦截
.and()
.formLogin().permitAll()//表单提交放行
.and()
.csrf().disable();//csrf关闭
}
// 注册PasswordEncoder
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
(5)自定义登录逻辑
service包下创建UserDetailsServiceImpl类,如下:
package com.sec.kun.service.Impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import com.sec.kun.pojo.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
/**
* @Author zhoukun
* @Date 2021/2/17 15:56
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//实际是根据用户名去数据库查,这里就直接用静态数据了
if(!username.equals("86547462")) {
throw new UsernameNotFoundException("用户名不存在!");
}
// 密码加密
String password = passwordEncoder.encode("123456");
//创建User用户,自定义的User
User user = new User(username,password, AuthorityUtils.
commaSeparatedStringToAuthorityList("admin"));
return user;
}
}
(6)认证服务配置
在config包下创建认证服务的配置类AuthorizationServerConfig,如下:
package com.sec.kun.config;
/**
* @Author zhoukun
* @Date 2021/2/17 15:59
*/
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
/**
* 授权服务器
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()//内存中
.withClient("client")//客户端ID
.secret(passwordEncoder.encode("zk2000208"))//秘钥
.redirectUris("https://www.bilibili.com")//重定向到的地址
.scopes("all")//授权范围
.authorizedGrantTypes("authorization_code");//授权类型为授权码模式
}
}
(7)资源服务配置
在config包下创建资源服务的配置类
/*
* 资源服务配置
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter{
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.requestMatchers()
.antMatchers("/user/**");
}
}
(8)controller
在controller包下创建UserController类,如下:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication) {
return authentication.getPrincipal();
}
}
测试
(1)获取授权码
启动项目,访问:http://localhost:8000/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all
说明:
http://localhost:8000:这是项目端口。
/oauth/authorize?response_type=code:获取授权码的固定写法。
client_id:这是客户端ID,就是在授权服务中定义的:
重定向到了百度首页,并且拿到了授权码。
(2)获取令牌
因为要发送post请求,所以使用postman。
url:http://localhost:8000/oauth/token
左边的type选择Basic Auth,右边的用户名为客户端ID,密码是定义好的。
发送请求,返回如下:
(3)获取资源服务器资源
需要携带通行令牌来获取。还是post请求:http://localhost:8000/user/getCurrentUser
环境搭建
直接在授权码模式的基础上进行修改了。
(1)修改SecurityConfig
直接在里面加:
//注册AuthenticationManager
@Bean
public AuthenticationManager getAuthenticationManager() throws Exception {
return super.authenticationManager();
}
(2)修改AuthorizationServerConfig
直接在里面加:
/**
* 密码模式
*/
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsServiceImpl userDetailsServiceImpl;
//密码模式需要配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsServiceImpl);
}
然后下面添加密码模式:
测试
(1)获取Token令牌
启动项目,直接在postman中发送:http://localhost:8000/oauth/token
(2)获取资源
现在直接可以携带令牌去访问资源:http://localhost:8000/user/getCurrentUser
将token直接存在内存中,这在生产环境中是不合理的,下面将其改造成存储在Redis中。
(1)pom依赖
在pom中添加如下依赖:
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
(2)yml配置
在application.yml中添加:
spring:
redis:
host: localhost
port: 6379
password: 123456
在AuthorizationServerConfig加入
/**
* redis工厂,默认使用lettue
*/
@Autowired
public RedisConnectionFactory redisConnectionFactory;
修改
(5)测试
启动工程,使用密码模式获取令牌:
查看redis:
SpringSecurity + OAuth2.0 + JWT前面只使用Oauth2.0的话,颁发的通行令牌长度太短了,现在想整合JWT,将颁发的token转换一下,转换成jwt格式的长令牌。
JWT:JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。
整合JWT
直接在此工程的基础上修改了。
pom
(2)Redis配置类
注释掉Redis的配置类。
(3)Jwt配置类
在config包下创建JwtTokenStoreConfig配置类,如下:
package com.sec.kun.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/**
* @Author zhoukun
* @Date 2021/2/17 19:50
*/
@Configuration
public class JwtTokenStoreConfig {
//注册JwtAccessTokenConverter
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//配置jwt秘钥
jwtAccessTokenConverter.setSigningKey("zhoukun");
return jwtAccessTokenConverter;
}
//注册TokenStore
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
}
(4)修改授权配置类
修改AuthorizationServerConfig类,如下:
/**
*Jwt配置类
*/
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
TokenStore tokenStore;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsServiceImpl)
.tokenStore(tokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
;
}
(5)测试
使用密码模式获取jwt令牌,如下:
现在的access_token令牌的长度发生了变化,与它对应的是jti值。解析这个token值:
扩展JWT的内容
现在想往JWT令牌中添加自定义的内容,过程如下。
(1)Jwt内容增强器
创建一个jwt包,包下创建一个Jwt的内容增强器JwtTokenEnhancer,如下:
package com.sec.kun.hancer;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* @Author zhoukun
* @Date 2021/2/17 19:56
*/
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication oAuth2Authentication) {
//自定义的内容存到map中
Map<String,Object> map = new HashMap<>();
map.put("city","西安");
map.put("like","yu");
map.put("age",20);
map.put("name","泡泡茶壶");
//下转型
if(accessToken instanceof DefaultOAuth2AccessToken) {
DefaultOAuth2AccessToken defaultOAuth2AccessToken = (DefaultOAuth2AccessToken)accessToken;
defaultOAuth2AccessToken.setAdditionalInformation(map);
return defaultOAuth2AccessToken;
}
return null;
}
}
(2)修改Jwt配置类
修改Jwt配置类JwtTokenStoreConfig,如下:
//密码模式需要配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
//创建TokenEnhancerChain实例
TokenEnhancerChain tokenEnhancerChain=new TokenEnhancerChain();
List<TokenEnhancer> enhancerList=new ArrayList<>();
//配置jtv内容增强器
enhancerList.add(jwtTokenEnhancer);
enhancerList.add(jwtAccessTokenConverter);
tokenEnhancerChain.setTokenEnhancers(enhancerList);
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsServiceImpl)
.tokenStore(tokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(tokenEnhancerChain)
;
}
(4)测试
解析生成的jwt令牌:
JWT令牌的内容一般要在java程序中解析出来,以下演示过程。
(1)pom依赖
还是使用jjwt来解析。pom中添加依赖:
<!-- jjwt依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
(2)controller
修改UserController,如下:
@RequestMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication, HttpServletRequest request) {
//获取请求头的指定内容
String header = request.getHeader("Authorization");
//截取,去掉请求头的前6位,获取token
String token = header.substring(header.indexOf("bearer") + 7);
//解析Token,获取Claims对象
Claims claims = Jwts.parser()
.setSigningKey("zhoukun".getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(token)
.getBody();
return claims;
}
测试
获取令牌
带着令牌获取资源
获取到了,返回的是jwt令牌解析后的内容。
JWT刷新令牌
在Spring Cloud Security中使用oauth2时,如果令牌失效了,可以使用刷新令牌通过refresh_token的授权模式再次获取access_token,只需修改认证服务器的配置,添加refresh_token的授权模式即可。
修改授权服务配置类AuthorizationServerConfig,如下:
测试:1分钟后再发请求:
获取不到资源了,现在jwt通行令牌已经过期了。解决方法是加一个刷新令牌refresh_token。如下:
然后再启动工程获取令牌:
等1分钟,用通行令牌获取资源:
通行令牌过期了。现在使用刷新令牌直接从授权服务端获取新的通行令牌:
获取到了新的通行令牌和刷新令牌。同样的,通行令牌的有效期还是1分钟,刷新令牌是1小时,再用这个新的通行令牌获取资源:
资源获取成功。