目录
1.基本概念
2.授权码模式
3.密码模式
4.JWT加密令牌
5.SpringSecurityOauth2整合JWT
刷新令牌
6.SpringSecurityOauth2整合SSO
简介,Oauth协议为用户资源的授权提供了一个安全的,开放而又简易的标准,同时,任何第三方都可以使用Oauth认证服务,目前Oauth是2.0版本使用最为广泛.
分析一下网站使用vx认证的过程:
1.首先用户想访问资源,需要认证,使用第三方认证比如(vx,qq,新浪等等)
2.用户确认使用第三方认证,那么需要向对应的第三方请求授权码,拿到授权码后,再用拿到的授权码去授权服务器请求令牌
3.服务端校验令牌,如果成功则授予相应的权限访问资源,获取对应的资源或者个人信息.
Oauth2这个是为了方便安全的用户(第三方)登录用的。
一开始听见oauth2这个词肯定是很懵的。这是啥,鉴权用的?认证用的?授权用的?跟shiro(java)是一个东西吗?
其实oauth就是一个流程。我们根据这个流程的要求写代码、
oauth有一个授权服务器。是作为用户的认证用的。对于服务端来说只需实现一个oauth的授权服务器。对于用户来说(调用授权认证的研发)只需根据流程发请求就可以了。
Oauth 有四种实体。 下面以用QQ登陆微博为例。
资源所有者(resource owner) 我们(普通用户)
资源服务器(resource server) QQ的后台服务器(获取账号,昵称,头像等)。
应用程序(client) 微博这个平台
授权服务器(Authorization Server) 认证QQ & 授权用的。
oauth有四种方式。 目的是不把密码暴露给第三方。(即不把QQ账号密码暴露给微博)
1.授权码 authorization code 也是安全等级最高的一版。 支持refresh token 一般都用这个,微博QQ登录就是这种方式。
2.密码 password credentials 支持refresh token 这种安全等级低。谁知道client会不会偷摸存你密码不。一般内部使用
3.简化模式 implicit 不支持refresh token 这种没有获取code的步骤。请求就给token、 没get到这个的优势
4.客户端模式 client credentials 不支持refresh token 这种是被信任的内部client使用。一般内部平台间使用
注: client_id client_secret 是用来oauth server鉴别client用的。
下面详细说明一下各个方式。简化模式就不说了。
授权码模式就是通过在授权服务器获得验证码和令牌双重校验,然后进行授权访问.
1)添加依赖:
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.6.3
spring-sercrity-student
spring-sercrity-myself
1.0
17
17
2021.0.1
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.cloud
spring-cloud-starter-oauth2
2.2.5.RELEASE
javax.xml.bind
jaxb-api
2.3.0
com.sun.xml.bind
jaxb-core
2.3.0
com.sun.xml.bind
jaxb-impl
2.3.0
javax.activation
activation
1.1.1
org.springframework.cloud
spring-cloud-starter-security
2.2.4.RELEASE
org.springframework.boot
spring-boot-starter-web
加入SpringCloud依赖,而SpringCloud也整合了security和oauth2框架.
2)创建securityconfig:
/**
* 授权框架配置类
*/
@Configuration
//表示启动webSecurity
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
//放行oauth认证,登录认证,用户退出
.antMatchers("/oauth/**", "/login/**", "logout/**").permitAll()
//拦截未被放行的所有请求
.anyRequest().authenticated()
.and()
//表单认证全部放行
.formLogin().permitAll();
}
//返回一个加密器
@Bean
public PasswordEncoder getPw() {
return new BCryptPasswordEncoder();
}
}
3)自定义用户信息包装类
/**
* 用户信息包装类
*/
public class User implements UserDetails {
//用户名
private String userName;
//用户密码
private String password;
//用户权限
private List authorities;
public User(String userName, String password, List 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)自定义认证类
/**
* 自定义认证
*/
@Service
public class UserService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String encodePw = passwordEncoder.encode("123");
return new User(username,encodePw, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
这里只是演示,实际密码应该从数据库中查询,进行校验.
5)配置授权服务器
/**
* 授权服务器配置
*
* @author 秦杨
* @version 1.0
*/
@Configuration
//表示启用授权服务器配置
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//这里只是暂时写死,正常需要去授权服务器注册
//配置Client id
.withClient("admin")
//配置Client security(密钥)
.secret(passwordEncoder.encode("Vermouth2022"))
//设置Token失效时间
.accessTokenValiditySeconds(3600)
//重定向地址,用于授权成功后跳转
.redirectUris("http://www.baidu.com")
//授权范围,申请读取哪部分内容
.scopes("all")
//授权类型
.authorizedGrantTypes("authorization_code");
}
}
这里拿到的客户端id和密钥将在第二次令牌验证的时候同时进行验证!!!
6)配置资源服务器
/**
* 资源服务配置
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.requestMatchers()
//放行需要获取的对应的资源
.antMatchers("/user/**");
}
}
当token令牌验证成功后就放行需要的功能.
具体操作流程:
这只是模拟授权码登录,实际上一般都是我们去vx这种第三方获取权限,所以也是访问他们的授权服务器和资源管理器,而这是为了了解流程.
当用户需要第三方授权时,我们会使用oauth协议,同时让用户扫描或者登录或者人脸识别等等来认证用户身份,同时将这些认证的信息发送到授权服务器.
授权请求:http://localhost:8080/oauth/authorize?response_type=code&client_id=admin&redirect_uri=http://www.baidu.com&scope=all
这里url的信息必须跟授权服务器中定义的一致,否则会报错.
/oauth/是我们授权框架的接口
/authorize表示要进行授权,请求一个验证码,客户端id是admin,成功后跳转到百度,请求的权限范围是all.
当我们访问后,它会自动跳转到login界面,让我们进行认证
输入账号密码/扫码后,成功后就会成功进入到我们授权的页面里
Approve表示同意授权,Deny表示拒绝.
同意后我们就跳转到了指定的页面.
仔细看,页面的url中带了一个code,而其值就是我们授权服务器返回的一个验证码.
拿到验证码我们要再去授权服务器进行验证
要用post请求去发送信息,我们用postman去使用.
使用验证码认证,同时输入我们在授权服务器中写死的用户端id,这个正常情况应该要注册的,为了演示就写死了.
同时请求体中还要带上授权服务器需要的参数和返回的验证码.
点击发送后我们拿到一个授权服务器返回的token和一些信息.
{
"access_token": "5afbda45-62aa-4345-bd20-a4e647356cf2",
"token_type": "bearer",
"expires_in": 3599,
"scope": "all"
}
拿到了token之后,就可以去访问我们需要的资源了,带上这个token去进行请求
提交后就会访问我们需要的功能,然后拿到相应的资源,这就是授权码模式
除了授权码模式,还有一种方式是用密码模式来认证,这种适合企业内部的认证方式,虽然携带用户名和密码,但是只在内部使用也无大碍,用起来比授权码简单,快捷.
1.修改授权服务器配置
/**
* 授权服务器配置
*
*/
@Configuration
//表示启用授权服务器配置
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
//引入tokenstore
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;
//密码模式配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
/**
* 密码模式下,还是要进行一个密码授权
* 所以还要用到之前使用的AuthenticationManager
* 去Security配置类创建一个bean返回父类的授权器就好了,在这里注入使用就好
* 然后传入我们的自定义校验逻辑,对用户进行认证
*/
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userService);
}
/**
* 导入认证服务器端点配置和安全配置
* 有四种模式,通过修改authorizedGrantTypes()值来选择.
* 常用的是authorization_code(授权码模式)
* password(密码模式)
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//这里只是暂时写死,正常需要去授权服务器注册
//配置Client id
.withClient("admin")
//配置Client security(密钥)
.secret(passwordEncoder.encode("Vermouth2022"))
//设置Token失效时间
.accessTokenValiditySeconds(3600)
//重定向地址,用于授权成功后跳转
.redirectUris("http://www.baidu.com")
//授权范围,申请读取哪部分内容
.scopes("all")
//授权类型
.authorizedGrantTypes("password");
}
}
2.启动项目,使用密码模式进行认证授权.
服务器端注册的密码还是不进行改变,需要改变的是请求体(request body)中的内容
将认证模式改为password,并且加入用户名(username)和密码(password),还有一个授权范围就够了.
访问后拿到我们的一个token令牌
{
"access_token": "d2bbfc32-8fdf-4957-8d0e-fc8202e6e74f",
"token_type": "bearer",
"expires_in": 3599,
"scope": "all"
}
拿到之后就我们就可以携带这个token去进行服务的访问了.
通常我们令牌是由授权服务器去颁发一个token但是是未经过加密的,可能会造成安全隐患,而JWT提供token字符串加密.
jwt的组成:
1.Header
JWT头是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存
{
"alg": "HS256",
"typ": "JWT"
}
2.Payload
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
这些预定义的字段并不要求强制使用。除以上默认字段外,我们还可以自定义私有字段,一般会把包含用户信息的数据放到payload中,如下例:
{
"sub": "1234567890",
"name": "Vermouth",
"admin": true
}
请注意,默认情况下JWT是未加密的,因为只是采用base64算法,拿到JWT字符串后可以转换回原本的JSON数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到JWT中,以防止信息泄露。JWT只是适合在网络中传输一些非敏感的信息
3.Signature
签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个盐(secret(密钥))。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)生成签名.
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用.
分隔,就构成整个JWT对象
例子:JWT官网模拟查看
三个部分都以.来分割JWT加密部分信息.
1.首先添加jwt依赖,jwt其实是一个规范,而其他的是由各种语言去实现,Java中有很多实现jwt加密的开源库,其中jjwt支持最好
io.jsonwebtoken
jjwt
0.9.1
PS:jjwt在0.10版本以后发生了较大变化,pom依赖要引入多个
io.jsonwebtoken
jjwt-api
0.11.2
io.jsonwebtoken
jjwt-impl
0.11.2
runtime
io.jsonwebtoken
jjwt-jackson
0.11.2
runtime
2.JWT加密token
public class MyTestJob {
@Test
public void teToken() {
//创建JwtBulider对象
JwtBuilder jwtBuilder = Jwts.builder()
/**声明表示 对应jwt中的
* {"jti":"4092"}
*/
.setId("4092")
/**
* 主体,用户
* {"sub":"Vermouth"}
*/
.setSubject("Vermouth")
/**设置签发时间
* {"ita":"2022-4-11"}
*/
.setIssuedAt(new Date())
/**
* JWT算法
* 加密算法+盐
* 表示我们用HS256进行加密,Rainbow作为我们的盐
*/
.signWith(SignatureAlgorithm.HS256, "Rainbow");
//生成token
String token = jwtBuilder.compact();
System.out.println(token);
System.out.println("============================================================++++==============");
//对加密token进行分段的解析,输出
String[] someTokenBody = token.split("\\.");
System.out.println(Base64Codec.BASE64.decodeToString(someTokenBody[0]));
System.out.println(Base64Codec.BASE64.decodeToString(someTokenBody[1]));
System.out.println(Base64Codec.BASE64.decodeToString(someTokenBody[2]));
}
}
输出结果:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0MDkyIiwic3ViIjoiVmVybW91dGgiLCJpYXQiOjE2NTAyNDgyNDl9.F0W1e3PWs7jn8lO6VIS52-3vpCtk5p553PnTFFko2OI
============================================================++++==============
{"alg":"HS256"}
{"jti":"4092","sub":"Vermouth","iat":1650248249}
E�{sֳ���S�T���{�
�9��w>t�J6
在第三行解密是一串乱码,因为没有相应的盐(密钥)就无法对第三部分进行解析.
所以我们需要用JWT自带的方法进行对token整体的一个解析.
3.解析token
上面使用Base64解析了JWT加密后token的声明部分和负载部分,但是拿到的是json格式的数据,而JWT提供方法让我们直接获得这些数据.
public class MyTestJob {
@Test
public void teToken() {
//创建JwtBulider对象
JwtBuilder jwtBuilder = Jwts.builder()
/**声明表示 对应jwt中的
* {"jti":"4092"}
*/
.setId("4092")
/**
* 主体,用户
* {"sub":"Vermouth"}
*/
.setSubject("Vermouth")
/**设置签发时间
* {"ita":"2022-4-11"}
*/
.setIssuedAt(new Date())
/**
* JWT算法
* 加密算法+盐
* 表示我们用HS256进行加密,Rainbow作为我们的盐
*/
.signWith(SignatureAlgorithm.HS256, "Rainbow");
//生成token
String token = jwtBuilder.compact();
System.out.println(token);
System.out.println("============================================================++++==============");
// String[] someTokenBody = token.split("\\.");
// System.out.println(Base64Codec.BASE64.decodeToString(someTokenBody[0]));
// System.out.println(Base64Codec.BASE64.decodeToString(someTokenBody[1]));
// System.out.println(Base64Codec.BASE64.decodeToString(someTokenBody[2]));
resolveToken(token);
}
public void resolveToken(String token){
//解析token中的负载中声明的对象
Claims claims = Jwts.parser()
//拿到盐,进行解析
.setSigningKey("Rainbow")
.parseClaimsJws(token)
.getBody();
//拿到token中的id
System.out.println(claims.getId());
//拿到用户
System.out.println(claims.getSubject());
//拿到签发时间
System.out.println(claims.getIssuedAt());
}
}
实际上就是在解码功能上增加了一些功能,可以直接拿到数据了
输出:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0MDkyIiwic3ViIjoiVmVybW91dGgiLCJpYXQiOjE2NTAyNTAwNTV9.aFHQExkH4WjVxaopDLXuimW_DRprIyHil79TG4EN2Aw
============================================================++++==============
4092
Vermouth
Mon Apr 18 10:47:35 CST 2022
4.token过期校验
在我们实际开发中,token不可能永久存活,我们需要设置它的有效时间.
public class MyTestJob {
@Test
public void teToken() {
//设置一分钟后过期
long expTime = System.currentTimeMillis() + 60 * 1000;
//创建JwtBulider对象
JwtBuilder jwtBuilder = Jwts.builder()
/**声明表示 对应jwt中的
* {"jti":"4092"}
*/
.setId("4092")
/**
* 主体,用户
* {"sub":"Vermouth"}
*/
.setSubject("Vermouth")
/**设置签发时间
* {"ita":"2022-4-11"}
*/
.setIssuedAt(new Date())
/**
* JWT算法
* 加密算法+盐
* 表示我们用HS256进行加密,Rainbow作为我们的盐
*/
.signWith(SignatureAlgorithm.HS256, "Rainbow")
//设置过期时间
.setExpiration(new Date(expTime));
//生成token
String token = jwtBuilder.compact();
System.out.println(token);
System.out.println("============================================================++++==============");
// String[] someTokenBody = token.split("\\.");
// System.out.println(Base64Codec.BASE64.decodeToString(someTokenBody[0]));
// System.out.println(Base64Codec.BASE64.decodeToString(someTokenBody[1]));
// System.out.println(Base64Codec.BASE64.decodeToString(someTokenBody[2]));
resolveToken(token);
}
public void resolveToken(String token) {
//解析token中的负载中声明的对象
Claims claims = Jwts.parser()
//拿到盐,进行解析
.setSigningKey("Rainbow")
.parseClaimsJws(token)
.getBody();
//拿到token中的id
System.out.println(claims.getId());
//拿到用户
System.out.println(claims.getSubject());
//拿到签发时间
System.out.println(claims.getIssuedAt());
//过期时间
System.out.println(claims.getExpiration());
}
}
输出:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0MDkyIiwic3ViIjoiVmVybW91dGgiLCJpYXQiOjE2NTAyNTA1MzcsImV4cCI6MTY1MDI1MDU5N30.2VU0RSJzhDnhWsXL-6m_IpvHmtV5A8O6YmK54N_xyLo
============================================================++++==============
4092
Vermouth
Mon Apr 18 10:55:37 CST 2022
Mon Apr 18 10:56:37 CST 2022
Process finished with exit code 0
当token失去时效后就不可用了.
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2022-04-18T10:58:04Z. Current time: 2022-04-18T10:58:25Z, a difference of 21175 milliseconds. Allowed clock skew: 0 milliseconds.
过期后解析,会抛出ExpiredJwtException.
5.自定义负载声明
我们也可以自定义一些声明放入到JWT负载中.
public class MyTestJob {
@Test
public void teToken() {
//设置一分钟后过期
long expTime = System.currentTimeMillis() + 60 * 1000;
//创建JwtBulider对象
JwtBuilder jwtBuilder = Jwts.builder()
/**声明表示 对应jwt中的
* {"jti":"4092"}
*/
.setId("4092")
/**
* 主体,用户
* {"sub":"Vermouth"}
*/
.setSubject("Vermouth")
/**设置签发时间
* {"ita":"2022-4-11"}
*/
.setIssuedAt(new Date())
/**
* JWT算法
* 加密算法+盐
* 表示我们用HS256进行加密,Rainbow作为我们的盐
*/
.signWith(SignatureAlgorithm.HS256, "Rainbow")
//设置过期时间
.setExpiration(new Date(expTime))
//添加自定义声明
.claim("authorize", "admin")
/**
* 也可以使用addClaims(map)
* 传入一个map来进行声明
*/
.claim("redirect", "user/order");
//生成token
String token = jwtBuilder.compact();
System.out.println(token);
System.out.println("============================================================++++==============");
// String[] someTokenBody = token.split("\\.");
// System.out.println(Base64Codec.BASE64.decodeToString(someTokenBody[0]));
// System.out.println(Base64Codec.BASE64.decodeToString(someTokenBody[1]));
// System.out.println(Base64Codec.BASE64.decodeToString(someTokenBody[2]));
resolveToken(token);
}
public void resolveToken(String token) {
//解析token中的负载中声明的对象
Claims claims = Jwts.parser()
//拿到盐,进行解析
.setSigningKey("Rainbow")
.parseClaimsJws(token)
.getBody();
//拿到token中的id
System.out.println(claims.getId());
//拿到用户
System.out.println(claims.getSubject());
//拿到签发时间
System.out.println(claims.getIssuedAt());
//过期时间
System.out.println(claims.getExpiration());
//拿去自定义声明
System.out.println(claims.get("authorize"));
System.out.println("redirect");
}
}
输出:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0MDkyIiwic3ViIjoiVmVybW91dGgiLCJpYXQiOjE2NTAyNTEyMjAsImV4cCI6MTY1MDI1MTI4MCwiYXV0aG9yaXplIjoiYWRtaW4iLCJyZWRpcmVjdCI6InVzZXIvb3JkZXIifQ.MMtD5uarftp_d6m1EhYoZwDxMQDK1WjiHEkO1pOGgO4
============================================================++++==============
4092
Vermouth
Mon Apr 18 11:07:00 CST 2022
Mon Apr 18 11:08:00 CST 2022
admin
redirectProcess finished with exit code 0
在Oauth2架构的密码模式中,我们需要将用户的一些信息封装在token中方便读取权限等进行授权或者认证,而JWT则可以提供这种支持.
1)JWT替换授权服务器令牌端点Token
如果我们要使用JWT来生成token令牌则需要将原来默认实现的令牌生成器进行替换.
在令牌端点配置TokenStore来进行替换.
首先,我们需要创建一个TokenStore.用来存储令牌生成器.
/**
* JWT Token配置类
*/
@Configuration
public class JwtTokenStoreConfig {
//配置jwt存储
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
//token生成处理
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//设置JWT的盐
jwtAccessTokenConverter.setSigningKey("Rainbow");
return jwtAccessTokenConverter;
}
//jwt增强器
@Bean
public JwtTokenEnhancer getJwtTokenEnhancer() {
return new JwtTokenEnhancer();
}
}
将jwt的token生成器放入到JwtTokenStore中,然后再将这个包装类返回.
在令牌端点中注入使用.
@Configuration
//表示启用授权服务器配置
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
//引入token store存储器
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;
//jwt token生成器
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
//引入jwt增强器
@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;
//密码模式配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//配置JWT内容增强器
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List delegates = new ArrayList<>();
//将增强内容和token生成器放入进去,到时生成token的时候就会将需要的内容放入jwt的负载中
delegates.add(jwtTokenEnhancer);
delegates.add(accessTokenConverter);
//将定义好的增强器放入jwt内容增强器
enhancerChain.setTokenEnhancers(delegates);
/**
* 密码模式下,还是要进行一个密码授权
* 所以还要用到之前使用的AuthenticationManager
* 去Security配置类创建一个bean返回父类的授权器就好了,在这里注入使用就好
* 然后传入我们的自定义校验逻辑,对用户进行认证
*/
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userService)
//配置令牌存储策略
.tokenStore(tokenStore)
//token生成器
.accessTokenConverter(accessTokenConverter)
//token内容增强器
.tokenEnhancer(enhancerChain);
}
/**
* 导入令牌端点配置和安全配置
* 有四种模式,通过修改authorizedGrantTypes()值来选择.
* 常用的是authorization_code(授权码模式)
* password(密码模式)
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//这里只是暂时写死,正常需要去授权服务器注册
//配置Client id
.withClient("Ash")
//配置Client security(密钥)
.secret(passwordEncoder.encode("Vermouth2022"))
//设置Token失效时间
.accessTokenValiditySeconds(3600)
//重定向地址,用于授权成功后跳转
.redirectUris("http://www.baidu.com")
//授权范围,申请读取哪部分内容
.scopes("all")
//授权类型
.authorizedGrantTypes("password");
}
}
在我们使用JWT加密方式生成Token的时候,我们也可以自定义内容,在上面注入了一个JWT token增强器,JwtEnhancer这是我们自己写的一个实现类,实现了TokenEnhaner,用来在JWT生成的token负载中增加自定义数据的。
/**
* JWT内容增强器
*/
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map message = new HashMap();
message.put("admin", "true");
//由于这里是一个接口,并没有直接实现的方法,所以要进行强转成默认实现然后添加
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(message);
return oAuth2AccessToken;
}
}
而配置完增强器,我们还需要在令牌端点中进行配置。
从上面拿一段主要的代码下来看看
//配置JWT内容增强器
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List delegates = new ArrayList<>();
//将增强内容和token生成器放入进去,到时生成token的时候就会将需要的内容放入jwt的负载中
delegates.add(jwtTokenEnhancer);
delegates.add(accessTokenConverter);
//将定义好的增强器放入jwt内容增强器
enhancerChain.setTokenEnhancers(delegates);
/**
* 密码模式下,还是要进行一个密码授权
* 所以还要用到之前使用的AuthenticationManager
* 去Security配置类创建一个bean返回父类的授权器就好了,在这里注入使用就好
* 然后传入我们的自定义校验逻辑,对用户进行认证
*/
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userService)
//配置令牌存储策略
.tokenStore(tokenStore)
//token生成器
.accessTokenConverter(accessTokenConverter)
//token内容增强器
.tokenEnhancer(enhancerChain);
首先我们需要一个内容增强器,另外就是创建一个TokenStore集合,将我们的增强器和token生成器放进去,这样到时内容增强器在使用的时候就会用token生成器将增强内容转化到token中。
最后在端点中将内容增强器放入就会自己将增强器中的内容放入到token中了。
2)Token内容解析
在上面,我们通过配置JWT增强器和端点的token令牌存储策略,使用JWT生成了token并且可以放入自定义内容,但是我们需要进行解析才能拿到数据,不肯能每次都copy然后去jwt官网查询,所以要用jwt依赖中提供的解析进行token的解密。
1.在我们的请求头中定义一个Authorization,用来读取token。
@RequestMapping("/user")
@RestController
public class UserController {
/**
* 获取当前用户
*
* @param authentication
* @return
*/
@RequestMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication, HttpServletRequest request) {
String head = request.getHeader("Authorization");
//在请求头的value中,用bearer开头中间空格后面就是我们的token
String token = head.substring(head.indexOf("bearer") + 7);
return Jwts.parser()
//设置密钥,保证是UTF-8读取,防止乱码
.setSigningKey("Rainbow".getBytes(StandardCharsets.UTF_8))
//需要解析的token
.parseClaimsJws(token)
//拿到token中负载部分的数据
.getBody();
}
}
2.用postman进行访问
之前我们需要带上token,还有在请求体中带上账号和密码去进行访问,而现在我们只需要在请求头上带上Authorization就行了。
而Authorization的值就是我们标记的bearer “token”,我们可以从bearer下标开始数7个然后就能读到我们的token。
在Oauth2框架中,除了密码模式和授权码模式常用以外,还有一个就是刷新令牌,刷新令牌可以让我们不用为token失效后又要去登录,只需要请求刷新令牌就可以获得新的token。
1.添加授权类型
/**
* 授权服务器配置
*/
@Configuration
//表示启用授权服务器配置
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
//引入token store存储器
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;
//jwt token生成器
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
//引入jwt增强器
@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;
//密码模式配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//配置JWT内容增强器
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List delegates = new ArrayList<>();
//将增强内容和token生成器放入进去,到时生成token的时候就会将需要的内容放入jwt的负载中
delegates.add(jwtTokenEnhancer);
delegates.add(accessTokenConverter);
//将定义好的增强器放入jwt内容增强器
enhancerChain.setTokenEnhancers(delegates);
/**
* 密码模式下,还是要进行一个密码授权
* 所以还要用到之前使用的AuthenticationManager
* 去Security配置类创建一个bean返回父类的授权器就好了,在这里注入使用就好
* 然后传入我们的自定义校验逻辑,对用户进行认证
*/
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userService)
//配置令牌存储策略
.tokenStore(tokenStore)
//token生成器
.accessTokenConverter(accessTokenConverter)
.tokenEnhancer(enhancerChain);
}
/**
* 导入令牌端点配置和安全配置
* 有四种模式,通过修改authorizedGrantTypes()值来选择.
* 常用的是authorization_code(授权码模式)
* password(密码模式)
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//这里只是暂时写死,正常需要去授权服务器注册
//配置Client id
.withClient("Ash")
//配置Client security(密钥)
.secret(passwordEncoder.encode("Vermouth2022"))
//设置Token失效时间
.accessTokenValiditySeconds(3600)
//重定向地址,用于授权成功后跳转
.redirectUris("http://www.baidu.com")
//授权范围,申请读取哪部分内容
.scopes("all")
//授权类型,可以有多个类型同时存在
.authorizedGrantTypes("password", "refresh_token", "authorization_code");
}
}
在上面加入刷新令牌。然后通过postman去访问
首先通过密码模式申请一个令牌,当我们打开referesh_token功能后,会产生一个刷新令牌
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhYmMiLCJzY29wZSI6WyJhbGwiXSwiYWRtaW4iOiJ0cnVlIiwiZXhwIjoxNjUwMjcyOTA3LCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiJhYzUwYzExMC03MjFiLTRjMDQtOWE5ZC1mZThmMWI4MzdiNTUiLCJjbGllbnRfaWQiOiJBc2gifQ.YhpE9PiwsjnsNcPcebSAS9TriiNwLt2-m_RysZ6uSmk",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhYmMiLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoiYWM1MGMxMTAtNzIxYi00YzA0LTlhOWQtZmU4ZjFiODM3YjU1IiwiYWRtaW4iOiJ0cnVlIiwiZXhwIjoxNjUyODYxMzA3LCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiJmNDM2YWNmOS03ZjE4LTQ4ZjEtYTcxYy05MjQzNTVmNDFiNjMiLCJjbGllbnRfaWQiOiJBc2gifQ.ChgfI_hKALVq9VmDE-iO2va5AGWlpE5Kzq9xNag-2ss",
"expires_in": 3599,
"scope": "all",
"admin": "true",
"jti": "ac50c110-721b-4c04-9a9d-fe8f1b837b55"
}
当我们token失效的时候我们可以用刷新令牌去重新申请一个新的令牌。
PS:但是需要注意,无论什么样的操作我们都需要登录Authorization,就是我们定义在授权服务器的用户名和密码进行授权服务器的登录,否则无法使用功能。
1.创建单点登录的客户端,引入需要的依赖
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.6.3
spring-security-sso
springsecutirysso-study
1.0
17
17
2021.0.1
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.cloud
spring-cloud-starter-oauth2
2.2.5.RELEASE
io.jsonwebtoken
jjwt
0.9.1
javax.xml.bind
jaxb-api
2.3.0
com.sun.xml.bind
jaxb-core
2.3.0
com.sun.xml.bind
jaxb-impl
2.3.0
javax.activation
activation
1.1.1
org.springframework.cloud
spring-cloud-starter-security
2.2.4.RELEASE
org.springframework.boot
spring-boot-starter-web
2.在启动类上添加@EnableOAuth2Sso
@SpringBootApplication
//开启单点登录功能
@EnableOAuth2Sso
public class SsoApplication {
public static void main(String[] args) {
SpringApplication.run(SsoApplication.class, args);
}
}
3.配置文件
server.port=8081
#防止Cookie冲突
server.servlet.session.cookie.name=OAUTH2-CLIENT-SESSIONID01
#授权服务器地址
oauth2-server-url:http://127.0.0.1:8080
#授权服务器对应的配置
security.oauth2.client.client-id=Ash
security.oauth2.client.client-secret=Vermouth2022
#获取授权的uri
security.oauth2.client.user-authorization-uri=${oauth2-server-url}/oauth/authorize
#获取token的uri
security.oauth2.client.access-token-uri=${oauth2-server-url}/oauth/token
#获取jwt的uri
security.oauth2.resource.jwt.key-uri=${oauth2-server-url}/oauth/token_key
4.在授权服务器中添加单点登录的配置信息,并且修改跳转网页.
/**
* 授权服务器配置
*/
@Configuration
//表示启用授权服务器配置
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
//引入token store存储器
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;
//jwt token生成器
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
//引入jwt增强器
@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;
//密码模式配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//配置JWT内容增强器
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List delegates = new ArrayList<>();
//将增强内容和token生成器放入进去,到时生成token的时候就会将需要的内容放入jwt的负载中
delegates.add(jwtTokenEnhancer);
delegates.add(accessTokenConverter);
//将定义好的增强器放入jwt内容增强器
enhancerChain.setTokenEnhancers(delegates);
/**
* 密码模式下,还是要进行一个密码授权
* 所以还要用到之前使用的AuthenticationManager
* 去Security配置类创建一个bean返回父类的授权器就好了,在这里注入使用就好
* 然后传入我们的自定义校验逻辑,对用户进行认证
*/
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userService)
//配置令牌存储策略
.tokenStore(tokenStore)
//token生成器
.accessTokenConverter(accessTokenConverter)
.tokenEnhancer(enhancerChain);
}
/**
* 导入令牌端点配置和安全配置
* 有四种模式,通过修改authorizedGrantTypes()值来选择.
* 常用的是authorization_code(授权码模式)
* password(密码模式)
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//这里只是暂时写死,正常需要去授权服务器注册
//配置Client id
.withClient("Ash")
//配置Client security(密钥)
.secret(passwordEncoder.encode("Vermouth2022"))
//设置Token失效时间
.accessTokenValiditySeconds(3600)
//刷新令牌有效期
.refreshTokenValiditySeconds(70000)
//重定向地址,用于授权成功后跳转
.redirectUris("http://localhost:8081/login")
//自动同意授权
.autoApprove(true)
//授权范围,申请读取哪部分内容
.scopes("all")
//授权类型,可以有多个类型同时存在
.authorizedGrantTypes("password", "refresh_token", "authorization_code");
}
//单点登录配置信息
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//获取密钥需要身份验证,使用单点登录必须配置
security.tokenKeyAccess("isAuthenticated()");
}
}
5.启动客户端和授权服务器,然后直接去访问客户端的资源,它会自动跳转到我们授权服务器进行账号密码登录,登录成功后就会提示是否授权,最后跳转回我们的资源页面允许访问资源。
总结:其实在这里,我们如果需要控制资源访问,通过业务拆分,将认证和授权放入到授权服务器中,我们只需要加入单点验证功能就可以对该服务下的所有资源进行控制,当我们访问单点服务资源的时候会跳转到我们配置好的授权服务器,并且进行登录认证,认证后进行授权,授权后会跳转到资源页面访问,其实中间的token那些在授权服务器就已经完成了.