OAuth2帮我们生成的JWT令牌载荷部分信息有限,关于用户信息只有一个user_name,有些场景下我们希望放入一些扩展信息项,比如,之前我们经常向session中存入userId,或者现在我希望在JWT的载荷部分存入当时请求令牌的客户端IP,客户端携带令牌访问资源服务时,可以对比当前请求的客户端真实IP和令牌中存放的客户端IP是否匹配,不匹配拒绝请求,以此进一步提高安全性。那么如何在OAuth2环境下向JWT令牌中存如扩展信息?
继承DefaultAccessTokenConverter类,重写convertAccessToken方法存入扩展信息
- package com.lagou.edu.config;
-
- import org.springframework.security.oauth2.common.OAuth2AccessToken;
- import org.springframework.security.oauth2.provider.OAuth2Authentication;
- import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
- import org.springframework.stereotype.Component;
- import org.springframework.web.context.request.RequestContextHolder;
- import org.springframework.web.context.request.ServletRequestAttributes;
-
- import javax.servlet.http.HttpServletRequest;
- import java.util.Map;
-
-
- @Component
- public class LagouAccessTokenConvertor extends DefaultAccessTokenConverter {
-
-
- @Override
- public Map
convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { - // 获取到request对象
- HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest();
- // 获取客户端ip(注意:如果是经过代理之后到达当前服务的话,那么这种方式获取的并不是真实的浏览器客户端ip)
- String remoteAddr = request.getRemoteAddr();
- Map
stringMap = (Map) super.convertAccessToken(token, authentication); - stringMap.put("clientIp",remoteAddr);
- return stringMap;
- }
- }
(2)将自定义的转换器对象注入

- package com.lagou.edu.config;
-
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.http.HttpMethod;
- import org.springframework.security.authentication.AuthenticationManager;
- import org.springframework.security.jwt.crypto.sign.MacSigner;
- import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
- import org.springframework.security.jwt.crypto.sign.Signer;
- 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;
- import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
- import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
- import org.springframework.security.oauth2.provider.ClientDetailsService;
- import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
- import org.springframework.security.oauth2.provider.token.*;
- import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
- import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
- import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
-
- import javax.sql.DataSource;
- import java.util.ArrayList;
- import java.util.List;
-
-
- /**
- * 当前类为Oauth2 server的配置类(需要继承特定的父类 AuthorizationServerConfigurerAdapter)
- */
- @Configuration
- @EnableAuthorizationServer // 开启认证服务器功能
- public class OauthServerConfiger extends AuthorizationServerConfigurerAdapter {
-
-
- @Autowired
- private AuthenticationManager authenticationManager;
-
- @Autowired
- private LagouAccessTokenConvertor lagouAccessTokenConvertor;
-
-
- private String sign_key = "lagou123"; // jwt签名密钥
-
-
- /**
- * 认证服务器最终是以api接口的方式对外提供服务(校验合法性并生成令牌、校验令牌等)
- * 那么,以api接口方式对外的话,就涉及到接口的访问权限,我们需要在这里进行必要的配置
- * @param security
- * @throws Exception
- */
- @Override
- public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
- super.configure(security);
- // 相当于打开endpoints 访问接口的开关,这样的话后期我们能够访问该接口
- security
- // 允许客户端表单认证
- .allowFormAuthenticationForClients()
- // 开启端口/oauth/token_key的访问权限(允许)
- .tokenKeyAccess("permitAll()")
- // 开启端口/oauth/check_token的访问权限(允许)
- .checkTokenAccess("permitAll()");
- }
-
- /**
- * 客户端详情配置,
- * 比如client_id,secret
- * 当前这个服务就如同QQ平台,拉勾网作为客户端需要qq平台进行登录授权认证等,提前需要到QQ平台注册,QQ平台会给拉勾网
- * 颁发client_id等必要参数,表明客户端是谁
- * @param clients
- * @throws Exception
- */
- @Override
- public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
- super.configure(clients);
-
-
- // 从内存中加载客户端详情
-
- /*clients.inMemory()// 客户端信息存储在什么地方,可以在内存中,可以在数据库里
- .withClient("client_lagou") // 添加一个client配置,指定其client_id
- .secret("abcxyz") // 指定客户端的密码/安全码
- .resourceIds("autodeliver") // 指定客户端所能访问资源id清单,此处的资源id是需要在具体的资源服务器上也配置一样
- // 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定
- .authorizedGrantTypes("password","refresh_token")
- // 客户端的权限范围,此处配置为all全部即可
- .scopes("all");*/
-
- // 从数据库中加载客户端详情
- clients.withClientDetails(createJdbcClientDetailsService());
-
- }
-
- @Autowired
- private DataSource dataSource;
-
- @Bean
- public JdbcClientDetailsService createJdbcClientDetailsService() {
- JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
- return jdbcClientDetailsService;
- }
-
-
- /**
- * 认证服务器是玩转token的,那么这里配置token令牌管理相关(token此时就是一个字符串,当下的token需要在服务器端存储,
- * 那么存储在哪里呢?都是在这里配置)
- * @param endpoints
- * @throws Exception
- */
- @Override
- public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
- super.configure(endpoints);
- endpoints
- .tokenStore(tokenStore()) // 指定token的存储方法
- .tokenServices(authorizationServerTokenServices()) // token服务的一个描述,可以认为是token生成细节的描述,比如有效时间多少等
- .authenticationManager(authenticationManager) // 指定认证管理器,随后注入一个到当前类使用即可
- .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
- }
-
-
- /*
- 该方法用于创建tokenStore对象(令牌存储对象)
- token以什么形式存储
- */
- public TokenStore tokenStore(){
- //return new InMemoryTokenStore();
- // 使用jwt令牌
- return new JwtTokenStore(jwtAccessTokenConverter());
- }
-
- /**
- * 返回jwt令牌转换器(帮助我们生成jwt令牌的)
- * 在这里,我们可以把签名密钥传递进去给转换器对象
- * @return
- */
- public JwtAccessTokenConverter jwtAccessTokenConverter() {
- JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
- jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
- jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使用的密钥,和签名密钥保持一致
- jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
-
- return jwtAccessTokenConverter;
- }
-
-
-
-
- /**
- * 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
- */
- public AuthorizationServerTokenServices authorizationServerTokenServices() {
- // 使用默认实现
- DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
- defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
- defaultTokenServices.setTokenStore(tokenStore());
-
- // 针对jwt令牌的添加
- defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
-
- // 设置令牌有效时间(一般设置为2个小时)
- defaultTokenServices.setAccessTokenValiditySeconds(20); // access_token就是我们请求资源需要携带的令牌
- // 设置刷新令牌的有效时间
- defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
-
- return defaultTokenServices;
- }
- }
资源服务器也需要自定义一个转换器类,继承DefaultAccessTokenConverter,重写extractAuthentication提取方法,把载荷信息设置到认证对象的details属性中。
- package com.lagou.edu.config;
-
- import org.springframework.security.oauth2.common.OAuth2AccessToken;
- import org.springframework.security.oauth2.provider.OAuth2Authentication;
- import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
- import org.springframework.stereotype.Component;
- import org.springframework.web.context.request.RequestContextHolder;
- import org.springframework.web.context.request.ServletRequestAttributes;
-
- import javax.servlet.http.HttpServletRequest;
- import java.util.Map;
-
-
- @Component
- public class LagouAccessTokenConvertor extends DefaultAccessTokenConverter {
-
-
- @Override
- public OAuth2Authentication extractAuthentication(Map
map) { -
- OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
- oAuth2Authentication.setDetails(map); // 将map放入认证对象中,认证对象在controller中可以拿到
- return oAuth2Authentication;
- }
- }
将自定义的转换器对象注入

- package com.lagou.edu.config;
-
- import org.springframework.beans.factory.annotation.Autowired;
- 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.http.SessionCreationPolicy;
- import org.springframework.security.jwt.crypto.sign.MacSigner;
- import org.springframework.security.jwt.crypto.sign.RsaVerifier;
- import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
- import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
- import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
- import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
- import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
- 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;
-
- @Configuration
- @EnableResourceServer // 开启资源服务器功能
- @EnableWebSecurity // 开启web访问安全
- public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
-
- private String sign_key = "lagou123"; // jwt签名密钥
-
- @Autowired
- private LagouAccessTokenConvertor lagouAccessTokenConvertor;
-
- /**
- * 该方法用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜
- * @param resources
- * @throws Exception
- */
- @Override
- public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
-
- /*// 设置当前资源服务的资源id
- resources.resourceId("autodeliver");
- // 定义token服务对象(token校验就应该靠token服务对象)
- RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
- // 校验端点/接口设置
- remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
- // 携带客户端id和客户端安全码
- remoteTokenServices.setClientId("client_lagou");
- remoteTokenServices.setClientSecret("abcxyz");
- // 别忘了这一步
- resources.tokenServices(remoteTokenServices);*/
-
-
- // jwt令牌改造
- resources.resourceId("autodeliver").tokenStore(tokenStore()).stateless(true);// 无状态设置
- }
-
-
- /**
- * 场景:一个服务中可能有很多资源(API接口)
- * 某一些API接口,需要先认证,才能访问
- * 某一些API接口,压根就不需要认证,本来就是对外开放的接口
- * 我们就需要对不同特点的接口区分对待(在当前configure方法中完成),设置是否需要经过认证
- *
- * @param http
- * @throws Exception
- */
- @Override
- public void configure(HttpSecurity http) throws Exception {
- http // 设置session的创建策略(根据需要创建即可)
- .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
- .and()
- .authorizeRequests()
- .antMatchers("/autodeliver/**").authenticated() // autodeliver为前缀的请求需要认证
- .antMatchers("/demo/**").authenticated() // demo为前缀的请求需要认证
- .anyRequest().permitAll(); // 其他请求不认证
- }
-
-
- /*
- 该方法用于创建tokenStore对象(令牌存储对象)
- token以什么形式存储
- */
- public TokenStore tokenStore(){
- //return new InMemoryTokenStore();
-
- // 使用jwt令牌
- return new JwtTokenStore(jwtAccessTokenConverter());
- }
-
- /**
- * 返回jwt令牌转换器(帮助我们生成jwt令牌的)
- * 在这里,我们可以把签名密钥传递进去给转换器对象
- * @return
- */
- public JwtAccessTokenConverter jwtAccessTokenConverter() {
- JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
- jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
- jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使用的密钥,和签名密钥保持一致
- jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor); //p
- return jwtAccessTokenConverter;
- }
-
- }
业务类比如Controller类中,可以通过SecurityContextHolder.getContext().getAuthentication()获取到认证对象,进一步获取到扩展信息
Object details =SecurityContextHolder.getContext().getAuthentication().getDetails();
获取到扩展信息后,就可以做其他的处理了,比如根据userId进一步处理,或者根据clientIp处理,或者其他都是可以的了。
关于JWT令牌我们需要注意