首先引入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
在授权端的代码,本文以jwt为例,增强token链
Auth协议(Token默认都是内存)
授权服务加入EnableAuthorizationServer这个注解,@Configuration类继承AuthorizationServerConfigurerAdapter,然后重写void configure(ClientDetailsServiceConfigurer clients
AuthorizationEndpoint:用来作为请求者获得授权的服务,默认的URL是/oauth/authorize.
TokenEndpoint:用来作为请求者获得令牌(Token)的服务,默认的URL是/oauth/token.
ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束.
AuthorizationServerEndpointsConfigurer:用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)。
配置授权服务一个比较重要的方面就是提供一个授权码给Oauth客户端(通过),这个授权码的获取是Oauth跳转到一个授权页面。通过雁阵授权知后服务器重定向到Oauth客户端,并且重定向向连接中附带一个授权码
ClientDetailsServiceConfigurer(AuthorizationServerConfigurer 一个回调项配置) 能够使用内春或者jdbc来实现客户端详情服务
客户端详情能够在程序运行的时候更新,也可以通过底层访问存放在关系数据库中,通过JdbcClientDetailsService管理
管理令牌(Managing Token):
AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,在使用这些操作的时候请注意以下几点:
当一个令牌被创建了,你必须对其进行保存,这样当一个客户端使用这个令牌对资源服务进行请求的时候才能够引用这个令牌。
当一个令牌是有效的时候,它可以被用来加载身份信息,里面包含了这个令牌的相关权限。
当你自己创建 AuthorizationServerTokenServices 这个接口的实现时,你可能需要考虑一下使用 DefaultTokenServices 这个类,里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了所有的事情。并且 TokenStore 这个接口有一个默认的实现,它就是 InMemoryTokenStore ,如其命名,所有的令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都实现了TokenStore接口:
InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。
JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时,你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的classpath当中。
JwtTokenStore:这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的
/**
* @ClassName: AuthorizationServerConfig
* @Description: 授权服务器配置类
* @Copyright: 2020 Prajna of Emergency. All rights reserved
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private TokenStore tokenStore;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserService userService;
@Autowired(required = false)
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired(required = false)
private TokenEnhancer jwtTokenEnhancer;
@Bean
public ClientDetailsService clientDetails(){
ClientDetailsService jdbcClientDetails=new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService)jdbcClientDetails).setPasswordEncoder(passwordEncoder);
return jdbcClientDetails;
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//访问tonkey,需要身份认证后,可以拿到签名密钥
security.tokenKeyAccess("isAuthenticated()")//允许token访问,原来permitAll()
.checkTokenAccess("permitAll()")//允许检查token
.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetails())
}
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setClientDetailsService(clientDetails());
//支持返回refresh——token。否则不返回refresh_token
defaultTokenServices.setSupportRefreshToken(true);
//封装在这里面 //配置token的存储方法
defaultTokenServices.setTokenStore(tokenStore);
//acess——token的有效期,如果数据库里面配置概知,这边江北覆盖
defaultTokenServices.setAccessTokenValiditySeconds(3000);
//数据库配置了的话,会覆盖该值
defaultTokenServices.setRefreshTokenValiditySeconds(6000);
//配置token返回
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
//配置token增强 普通token转换为jwt_token
enhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer,jwtAccessTokenConverter));
//配置返货jwt格式token转换
defaultTokenServices.setTokenEnhancer(enhancerChain);
return defaultTokenServices;
}
@Bean
public AuthorizationCodeServices authorizationCodeService() {
return new JdbcAuthorizationCodeServices(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore)
.authenticationManager(authenticationManager)//认证管理器
.authorizationCodeServices(authorizationCodeService())//授权码管理
.tokenServices(tokenServices());//token管理
//.userDetailsService(userService);//需要现实指定userDetailsService,否则refresh_token报错,并没有报错,我就舍弃了
if(jwtAccessTokenConverter!=null){
endpoints.accessTokenConverter(jwtAccessTokenConverter);
//至此jwt就替换了spring默认的token(有状态,通过uuid产生)
// 如果想添加额外的信息塞进jwt令牌可同股票(TokenEnhancer)
}
}
}
@Configuration
public class TokenConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
@ConditionalOnProperty(prefix = "type",name="storeType",havingValue = "redis")
public TokenStore tokenStore() {
//return new InMemoryTokenStore();
return new RedisTokenStore(redisConnectionFactory);
}
//修改产生token机制
@Configuration
@ConditionalOnProperty(prefix = "type",name="storeType",havingValue = "jwt",matchIfMissing = true)
public static class JwtTokenConfig{
@Autowired
private SecurityConf securityConf;
@Bean
public TokenStore tokenStore(){
System.out.println("c*********产生jwt密钥");
/**
* JwtTokenStore不管token的具体生成,而是交给jwtAccessTokenConverter去做
*/
return new JwtTokenStore(jwtAccessTokenConverter());
}
//密钥签发令牌
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter=new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("123456");//对称密钥,资源服务器需要和这个一致
return jwtAccessTokenConverter;
}
@Bean
/**
* 如果没有就注入
*/
@ConditionalOnMissingBean(name="jwtTokenEnhancer")
public TokenEnhancer jwtTokenEnhancer(){
System.out.println("c*********jwtTokenEnhancer");
return new JwtTokenEnhancer();
}
}
}
这个是可以添加你想要的信息,在token里面都可以解析出来
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String,Object> info=new HashMap<String,Object>();
info.put("company","qi");
info.put("test","测试");
((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(info);
return accessToken;
}
}
@Service
public class UserService implements UserDetailsService {
private Logger logger= LoggerFactory.getLogger(UserService.class);
@Autowired
private SysUserDao sysUserDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("loadUserByUsername: "+ username);
//权限,根据来查找
Map<String,Object> paraMap=new HashMap<String,Object>();
paraMap.put("name",username);
SysUser sysUser=null;
try {
List<SysUser> sysUserList=sysUserDao.listSysUser(paraMap);
if(sysUserList!=null&&sysUserList.size()>0){
sysUser=sysUserList.get(0);
}else{
throw new UsernameNotFoundException("用户名:"+username+" 不存在");
}
} catch (Exception e) {
if(e.getMessage().indexOf("用户名")!=-1){
throw new UsernameNotFoundException("用户名:"+username+" 不存在");
}
e.printStackTrace();
}
List<GrantedAuthority> authorities= new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("order::query"));
authorities.add(new SimpleGrantedAuthority("order::add"));
logger.info("登录的密码是:"+sysUser.getPassword());
UserDetails userDetail = new User(username, sysUser.getPassword(), authorities);
return userDetail;
}
}
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
}
https://blog.youkuaiyun.com/neosmith/article/details/52539927
资源服务加入EnableResourceServerr这个注解,整个项目就是资源服务;
一个资源服务(可以和授权服务在同一个应用中,当然也可以分离开成为两个不同的应用程序)提供一些受token令牌保护的资源,Spring OAuth提供者是通过Spring Security authentication filter 即验证过滤器来实现的保护,你可以通过 @EnableResourceServer 注解到一个 @Configuration 配置类上,并且必须使用 ResourceServerConfigurer 这个配置对象来进行配置(可以选择继承自 ResourceServerConfigurerAdapter 然后覆写其中的方法,参数就是这个对象的实例),下面是一些可以配置的属性
ResourceServerTokenServices 是组成授权服务的另一半,如果你的授权服务和资源服务在同一个应用程序上的话,你可以使用 DefaultTokenServices ,这样的话,你就不用考虑关于实现所有必要的接口的一致性问题,这通常是很困难的。如果你的资源服务器是分离开的,那么你就必须要确保能够有匹配授权服务提供的 ResourceServerTokenServices,它知道如何对令牌进行解码。
在授权服务器上,你通常可以使用 DefaultTokenServices 并且选择一些主要的表达式通过 TokenStore(后端存储或者本地编码)。
RemoteTokenServices 可以作为一个替代,它将允许资源服务器通过HTTP请求来解码令牌(也就是授权服务的 /oauth/check_token 端点)。如果你的资源服务没有太大的访问量的话,那么使用RemoteTokenServices 将会很方便(所有受保护的资源请求都将请求一次授权服务用以检验token值),或者你可以通过缓存来保存每一个token验证的结果。
esourceServerTokenServices 是组成授权服务的另一半,如果你的授权服务和资源服务在同一个应用程序上的话,你可以使用 DefaultTokenServices ,这样的话,你就不用考虑关于实现所有必要的接口的一致性问题,这通常是很困难的。如果你的资源服务器是分离开的,那么你就必须要确保能够有匹配授权服务提供的 ResourceServerTokenServices,它知道如何对令牌进行解码。
在授权服务器上,你通常可以使用 DefaultTokenServices 并且选择一些主要的表达式通过 TokenStore(后端存储或者本地编码)。
除了引入上面的jar还需要引入
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
在yml里面写下配置:
security:
oauth2:
resource:
jwt:
key-uri: http://127.0.0.1:9004/oauth/token_key
client:
client-id: resourceClient
client-secret: resource123
user-authorization-uri: http://127.0.0.1:9004/oauth/authorize
access-token-uri: http://127.0.0.1:9004/oauth/token
上代码:
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Bean
public ResourceServerTokenServices tokenServices() {
//向远程认证服务器获取token,同时获取token的用户信息
RemoteTokenServices remoteService = new RemoteTokenServices();
remoteService.setCheckTokenEndpointUrl("http://127.0.0.1:9004/oauth/check_token");
remoteService.setClientId("resourceClient");
remoteService.setClientSecret("resource123");
return remoteService;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("resourceRes")//资源标识
//.tokenStore(tokenStore)//本地令牌校验
.stateless(true);//关闭session
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/res/**").access("#oauth2.hasScope('resource')")
.anyRequest().authenticated()
.and().formLogin()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
参考:https://blog.youkuaiyun.com/A15712399740/article/details/95233903
jwt的基本思路就是用户提供用户名和密码给认证服务器,服务器验证用户提交信息信息的合法性;如果验证成功,会产生并返回一个Token(令牌),用户可以使用这个token访问服务器上受保护的资源。
对产生的token进行分析
当我们在springboot中映入spring-security的相关包,security会默认为我们创建WebSecurityConfigurerAdapter,他拦截所有的http请求(/**),且这个Order的值是:SecurityProperties.BASIC_AUTH_ORDER
Security默认会为我们创建一些filter
可以配置security.basic.enabled=false来让默认失效,或者可以定义一个order值更低的类,继承WebSecurityConfigurerAdapter
ResourceServerConfigurerAdapter优先级比另外一个更高,他会优先处理,而WebSecurityConfigurerAdapter会失效
security.oauth2.resource.filter-order=99也可使resource失效
注意:我们每声明一个*Adapter类,都会产生一个filterChain。前面我们讲到一个request(匹配url)只能被一个filterChain处理,这就解释了为什么二者Adapter同时在的时候,前者默认为什么会失效的原因。我们可以在FilterChainProxy中的getFilters(HttpServletRequest request)方法中可以看到有哪些filter chain,并处理哪些url
WebSecurityConfigurerAdapter:是对页面登录进行验证
ResourceServerConfigurerAdapter:对token验证
测试流程:
授权码模式访问,获取 code:
http://client:secret@localhost:9004/oauth/token(获取token)
得出结果:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2VSZXMiXSwidGVzdCI6Iua1i-ivlSIsInVzZXJfbmFtZSI6IuijtOmbheadsCIsInNjb3BlIjpbInJlc291cmNlIl0sImNvbXBhbnkiOiJxaSIsImV4cCI6MTU5NzcxOTY0OCwiYXV0aG9yaXRpZXMiOlsib3JkZXI6OmFkZCIsIm9yZGVyOjpxdWVyeSJdLCJqdGkiOiI3ZDFlNWRkZi1lOTQ0LTQzNWItYTlhOS0wZTYzM2E4MDVjN2MiLCJjbGllbnRfaWQiOiJyZXNvdXJjZUNsaWVudCJ9.aNQBxjIsrRaTBobX4HJ3BK5w9s12dKGl_cP-a7G9paY",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2VSZXMiXSwidGVzdCI6Iua1i-ivlSIsInVzZXJfbmFtZSI6IuijtOmbheadsCIsInNjb3BlIjpbInJlc291cmNlIl0sImF0aSI6IjdkMWU1ZGRmLWU5NDQtNDM1Yi1hOWE5LTBlNjMzYTgwNWM3YyIsImNvbXBhbnkiOiJxaSIsImV4cCI6MTU5NzcxOTgyNiwiYXV0aG9yaXRpZXMiOlsib3JkZXI6OmFkZCIsIm9yZGVyOjpxdWVyeSJdLCJqdGkiOiI3NDA1YjBhYy1jNzY0LTRhZWQtYmNmYi04N2Q5YTI1YzJlM2YiLCJjbGllbnRfaWQiOiJyZXNvdXJjZUNsaWVudCJ9.ipJTl3t9KOJ_dP3Dh2K6CHFuk14bHpg0sZ9zQEoXCLE",
"expires_in": 300,
"scope": "resource",
"test": "测试",
"company": "qi",
"jti": "7d1e5ddf-e944-435b-a9a9-0e633a805c7c"
}
密码模式访问
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2VSZXMiXSwidGVzdCI6Iua1i-ivlSIsInVzZXJfbmFtZSI6IuijtOmbheadsCIsInNjb3BlIjpbInJlc291cmNlIl0sImNvbXBhbnkiOiJxaSIsImV4cCI6MTU5NzcyMTg0MiwiYXV0aG9yaXRpZXMiOlsib3JkZXI6OmFkZCIsIm9yZGVyOjpxdWVyeSJdLCJqdGkiOiJmMWU2ZDllOC04ZTMxLTQ1OGYtYjA4Mi04NjQ3M2YwYmJhYTQiLCJjbGllbnRfaWQiOiJyZXNvdXJjZUNsaWVudCJ9.XW0SCEdVDR3Sg8D63tbHnznmQn26Wmxu15Zt26JTPj8",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2VSZXMiXSwidGVzdCI6Iua1i-ivlSIsInVzZXJfbmFtZSI6IuijtOmbheadsCIsInNjb3BlIjpbInJlc291cmNlIl0sImF0aSI6ImYxZTZkOWU4LThlMzEtNDU4Zi1iMDgyLTg2NDczZjBiYmFhNCIsImNvbXBhbnkiOiJxaSIsImV4cCI6MTU5NzcyMjAxOSwiYXV0aG9yaXRpZXMiOlsib3JkZXI6OmFkZCIsIm9yZGVyOjpxdWVyeSJdLCJqdGkiOiJlZjM0YmU1Yi0wZjg1LTQ2ZGItYjQ4Ny01NjYyMzQ0MjlhOTUiLCJjbGllbnRfaWQiOiJyZXNvdXJjZUNsaWVudCJ9.Gb7ErhTDg_Wp6_5WAuk9undfhYFqHDKKzM7KXE4823U",
"expires_in": 299,
"scope": "resource",
"test": "测试",
"company": "qi",
"jti": "f1e6d9e8-8e31-458f-b082-86473f0bbaa4"
}
获取用户信息
http://localhost:9002/iem-resource/res/auth/me1
{
"authorities": [
{
"authority": "order::add"
},
{
"authority": "order::query"
}
],
"details": {
"remoteAddress": "10.36.60.26",
"sessionId": null,
"tokenValue": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2VSZXMiXSwidGVzdCI6Iua1i-ivlSIsInVzZXJfbmFtZSI6IuijtOmbheadsCIsInNjb3BlIjpbInJlc291cmNlIl0sImNvbXBhbnkiOiJxaSIsImV4cCI6MTU5NzcyMTg0MiwiYXV0aG9yaXRpZXMiOlsib3JkZXI6OmFkZCIsIm9yZGVyOjpxdWVyeSJdLCJqdGkiOiJmMWU2ZDllOC04ZTMxLTQ1OGYtYjA4Mi04NjQ3M2YwYmJhYTQiLCJjbGllbnRfaWQiOiJyZXNvdXJjZUNsaWVudCJ9.XW0SCEdVDR3Sg8D63tbHnznmQn26Wmxu15Zt26JTPj8",
"tokenType": "bearer",
"decodedDetails": null
},
"authenticated": true,
"userAuthentication": {
"authorities": [
{
"authority": "order::add"
},
{
"authority": "order::query"
}
],
"details": null,
"authenticated": true,
"principal": "裴雅杰",
"credentials": "N/A",
"name": "裴雅杰"
},
"credentials": "",
"principal": "裴雅杰",
"clientOnly": false,
"oauth2Request": {
"clientId": "resourceClient",
"scope": [
"resource"
],
"requestParameters": {
"client_id": "resourceClient"
},
"resourceIds": [
"resourceRes"
],
"authorities": [],
"approved": true,
"refresh": false,
"redirectUri": null,
"responseTypes": [],
"extensions": {},
"grantType": null,
"refreshTokenRequest": null
},
"name": "裴雅杰"
}
刷新token
refresh_token的值就是前面返回的refresh_token放进去就好。