為什么使用jwt步鉴?
在原先dubbo+zookeeper項目中,web模塊只暴露Restful接口璃哟,各服務(wù)模塊只暴露duboo接口氛琢,此時用戶登錄后由web項目進(jìn)行token的鑒權(quán)和驗證,并通過dubbo的隱式傳參將sessionID傳遞給dubbo服務(wù)模塊, 攔截器再根據(jù)sessionID從Redis中獲取用戶信息設(shè)置到當(dāng)前線程
然鵝随闪,在springcloud中阳似,各個微服務(wù)直接暴露的是restful接口,此時如何讓各個微服務(wù)獲取到當(dāng)前用戶信息呢铐伴?最佳的方式就是token了撮奏,token作為BS之間的會話標(biāo)識(一般是原生隨機token),同時也可以作為信息的載體傳遞一些自定義信息(jwt当宴, 即Json web token)畜吊。
為了能更清楚的了解本文,需要對spring-security-oauth 及 jwt有一定了解户矢,本文只關(guān)注用戶信息傳遞這一塊
認(rèn)證服務(wù)器
認(rèn)證服務(wù)器配置AuthorizationServerConfigurerAdapter
@Configuration
@PropertySource({"classpath:application.yml"})
@EnableAuthorizationServer
public class AuthServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
TokenStore tokenStore;
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
ApprovalStore approvalStore;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.tokenStore(tokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
.approvalStore(approvalStore)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//允許表單認(rèn)證
security
.tokenKeyAccess("permitAll()") //url:/oauth/token_key,exposes public key for token verification if using JWT tokens
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
@Bean
@Primary
@ConfigurationProperties("ms-sql.datasource")
public DataSource dataSource() {
return new DriverManagerDataSource();
}
@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource());
}
/**
* 使用 Jwt token
* @param accessTokenConverter
* @return
*/
@Bean
public TokenStore tokenStore(@Autowired JwtAccessTokenConverter accessTokenConverter) {
return new JwtTokenStore(accessTokenConverter);
}
/**
* token 轉(zhuǎn)換器玲献,加入對稱秘鑰,使用自定tokenEnhancer
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new CustomJwtAccessTokenConverter();
converter.setSigningKey("secretKey");
return converter;
}
}
自定義token轉(zhuǎn)換器
CustomJwtAccessTokenConverter
/**
* 對JwtAccessTokenConverter 的 enhance進(jìn)行重寫梯浪,加入自定義的信息
*
* @author wangqichang
* @since 2019/4/26
*/
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
private static final String BEARER_PRIFIX = "bearer ";
//這個是token增強器捌年,想讓jwt token攜帶額外的信息在這里處理
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
if (accessToken instanceof DefaultOAuth2AccessToken) {
Object principal = authentication.getPrincipal();
//這個principal是當(dāng)時登錄后存到securiy的東東,一般是用戶實體驱证,自己debug一下就知道了
if (principal instanceof OAuthUser) {
OAuthUser user = (OAuthUser) principal;
HashMap<String, Object> map = new HashMap<>();
//jwt默認(rèn)已經(jīng)自帶用戶名延窜,無需再次加入
map.put("nick_name", user.getUsernickname());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
}
}
return super.enhance(accessToken, authentication);
}
//主要是資源服務(wù)器解析時一定要有bearer這個頭才認(rèn)為是一個oauth請求,但不知道為啥指定jwt后這個頭就不見了抹锄,特意加上去
@Override
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
return BEARER_PRIFIX + super.encode(accessToken, authentication);
}
}
此時按照固定格式訪問授權(quán)服務(wù)器token接口獲取token逆瑞,如圖荠藤,可以獲取到j(luò)wt格式的token,并且額外信息nick_name也已經(jīng)添加
直接解析jwt字符串可以獲取到以下信息获高,即用戶名和授權(quán)信息
資源服務(wù)器如何鑒權(quán)哈肖?
只需要指定和授權(quán)服務(wù)器一模一樣的token store 和token converter
在securiy的過濾器中OAuth2AuthenticationProcessingFilter
會從token中獲取相關(guān)信息進(jìn)行鑒權(quán)
源碼:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
boolean debug = logger.isDebugEnabled();
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
try {
//這里從請求中獲取到Authorization的 header
Authentication authentication = this.tokenExtractor.extract(request);
if (authentication == null) {
if (this.stateless && this.isAuthenticated()) {
if (debug) {
logger.debug("Clearing security context.");
}
SecurityContextHolder.clearContext();
}
if (debug) {
logger.debug("No token in request, will continue chain.");
}
} else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken)authentication;
needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
//這里將調(diào)用當(dāng)前設(shè)定的token Store 和 converter 將字符串token轉(zhuǎn)成Authentication
Authentication authResult = this.authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
this.eventPublisher.publishAuthenticationSuccess(authResult);
//認(rèn)證成功后,這里將安全上下文設(shè)置用戶信息念秧,一般包含用戶名和權(quán)限淤井。額外信息需要自定義處理,security不會幫你處理的
SecurityContextHolder.getContext().setAuthentication(authResult);
}
} catch (OAuth2Exception var9) {
SecurityContextHolder.clearContext();
if (debug) {
logger.debug("Authentication request failed: " + var9);
}
this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(var9.getMessage(), var9), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(var9.getMessage(), var9));
return;
}
chain.doFilter(request, response);
}
注意摊趾,資源服務(wù)器主要配置在
ResourceServerConfigurerAdapter
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Autowired
TokenStore tokenStore;
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Bean
public TokenStore tokenStore(@Autowired JwtAccessTokenConverter accessTokenConverter) {
return new JwtTokenStore(accessTokenConverter);
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("secretKey");
return converter;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore);
resources.tokenServices(defaultTokenServices);
super.configure(resources);
}
//其他資源不做限制币狠,讓security直接放行,這樣只限制注解了的方法
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").permitAll();
super.configure(http);
}
}
微服務(wù)獲取jwttoken中的用戶信息砾层,兩種方式漩绵,使用security上下文可以直接獲取當(dāng)前用戶名和權(quán)限,另一種自定義攔截器獲取額外信息肛炮。
這個就簡單了止吐,獲取header頭解析驗證token
然后獲取之前從授權(quán)服務(wù)器中的添加的 nick_name的額外信息放入線程變量
/**
* 用戶信息攔截器
* 從Header中取出jwttoken,并獲取其中的用戶名設(shè)置到用戶信息上下文線程變量中
*
* @author wangqichang
* @since 2019/4/24
*/
public class UserInfoInterceptor implements HandlerInterceptor {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String authorization = request.getHeader("Authorization");
authorization = StrUtil.removePrefix(authorization, "bearer ");
if (StrUtil.isNotBlank(authorization)) {
Jwt decode = JwtHelper.decode(authorization);
//驗簽
// Jwt secretKey = JwtHelper.decodeAndVerify(authorization, new MacSigner("secretKey"));
String claims = decode.getClaims();
HashMap<String, Object> hashMap = objectMapper.readValue(claims, HashMap.class);
Object userName = hashMap.get("user_name");
Object nickName = hashMap.get("nick_name");
Object authorities = hashMap.get("authorities");
UserContext.setUserInfo(UserInfo.builder().userName((String) userName).nickName((String) nickName).build());
}
return true;
}
}
其中用戶上下文類
/**
* @author wangqichang
* @since 2019/4/24
*/
public class UserContext {
private static ThreadLocal<UserInfo> threadLocal = new ThreadLocal<>();
public static UserInfo current() {
return threadLocal.get();
}
public static String currentUserName() {
UserInfo userInfo = threadLocal.get();
if (ObjectUtil.isNotNull(userInfo)) {
return userInfo.getUserName();
}
return null;
}
public static void setUserInfo(UserInfo userInfo) {
threadLocal.set(userInfo);
}
}
啟動攔截器注冊webmvc配置類
/**
* @author wangqichang
* @since 2019/4/25
*/
@Configuration
public class CustomWebMvcConfig extends WebMvcConfigurationSupport {
/**
* 注冊用戶信息攔截器
*
* @param registry
*/
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor()).addPathPatterns("/**");
super.addInterceptors(registry);
}
/**
* 當(dāng)自定義webmvc配置后侨糟,swagger無法讀取到靜態(tài)Resouce資源碍扔,需要手動添加
*
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
super.addResourceHandlers(registry);
}
}
在controller中獲取用戶信息如圖
自定義Oauth異常信息
在默認(rèn)的認(rèn)證異常如圖
假設(shè)我們做了全局異常處理,前端希望在token過期時做統(tǒng)一的登錄跳轉(zhuǎn)如何做秕重?
實現(xiàn)AuthenticationEntryPoint
接口重寫commence
方法即可
注意不同,直接拋出異常并不會走@RestControllerAdvice
, 因為在這里是response直接返回,并沒有使用到Controller處理
/**
* @author wangqichang
* @since 2019/4/30
*/
public class CustomOAuth2AuthenticationEntryPoint extends AbstractOAuth2SecurityExceptionHandler implements AuthenticationEntryPoint {
@Autowired
private ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Response<Object> build = Response.builder().respCode(StatusCode.TOKEN_ERR).respDesc(authException.getMessage()).build();
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(objectMapper.writeValueAsString(build));
}
@ConditionalOnMissingBean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
此時返回我自定義的Response對象溶耘,如圖