Spring Cloud OAuth2
代码地址:https://gitee.com/kkmy/kw-microservices.git
(又是一年1024,分享一下之前搭的OAuth2服务)
OAuth2依赖版本
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
代码工程结构
核心代码配置
SecurityConfig
- 密码模式配置 BCryptPasswordEncoder
- 自定义用户信息认证myUserDetailsService
- 暴露authenticationManagerBean
- 安全参数配置configure()
MyUserDetailsService
这里使用了策略模式,根据传来的系统类型,调用对应系统服务的接口
package pers.kw.config.security;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
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.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.stereotype.Component;
import pers.kw.common.spring.utils.SpringUtils;
import pers.kw.config.oauth.context.MyParamValue;
import pers.kw.config.oauth.context.MyParamValueThreadLocal;
import pers.kw.contants.AuthParamName;
import pers.kw.enums.AuthUserTypeEnum;
import pers.kw.service.UserDetailStrategy;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义UserDetailService
*/
@Component
public class MyUserDetailsService implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(MyUserDetailsService.class);
private static final List<GrantedAuthority> authorities = new ArrayList<>(2);
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
log.info("自定义UserDetailsService处理start...");
MyParamValue paramValue = MyParamValueThreadLocal.getCurrent();
log.info("获取自定义参数信息:{}", JSON.toJSONString(paramValue));
String userType = paramValue.getAuthParameter(AuthParamName.USER_TYPE);
if (StringUtils.isBlank(userType)) {
throw new OAuth2Exception(AuthParamName.USER_TYPE + "不能为空");
}
if (!AuthUserTypeEnum.userTypeSet.contains(userType)) {
throw new OAuth2Exception(AuthParamName.USER_TYPE + "错误");
}
AuthUserTypeEnum userTypeEnum = AuthUserTypeEnum.getEnumObjByCode(userType);
if (userTypeEnum == null) {
log.info("oauth服务,用户认证策略配置错误,{}:{}", AuthParamName.USER_TYPE, userType);
throw new OAuth2Exception("认证系统异常");
}
try {
UserDetailStrategy userDetailStrategy = (UserDetailStrategy) SpringUtils.getBean(Class.forName(userTypeEnum.getUserStrategy()));
return userDetailStrategy.getUserInfoByMobile(userName,authorities);
} catch (ClassNotFoundException e) {
log.error("oauth服务,用户认证策略配置获取异常", e);
throw new OAuth2Exception("认证系统异常");
}
}
}
AuthorizationServerConfig
- 授权服务安全认证配置configure(AuthorizationServerSecurityConfigurer security)
- 自定义客户端异常处理过滤器(basic方式认证)
- 客户端信息配置configure(ClientDetailsServiceConfigurer clients)
- 授权服务端点配置configure(AuthorizationServerEndpointsConfigurer endpoints)
- 自定义异常信息返回值(授权码模式、密码模式)
- 设置token请求方式
- token信息配置
通过添加自定义过滤器,实现对oauth标准接口增加自定义参数
MyOauthAuthenticationFilter
package pers.kw.config.oauth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
import pers.kw.config.oauth.context.MyParamValue;
import pers.kw.config.oauth.context.MyParamValueThreadLocal;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 通过添加自定义过滤器,实现对oauth标准接口增加自定义参数
*/
@Component
public class MyOauthAuthenticationFilter extends GenericFilterBean implements ApplicationContextAware {
private static final Logger log = LoggerFactory.getLogger(MyOauthAuthenticationFilter.class);
private ApplicationContext applicationContext;
private final RequestMatcher requestMatcher;
private static final String URL = "/oauth/token";
public MyOauthAuthenticationFilter() {
this.requestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(URL, "GET"),
new AntPathRequestMatcher(URL, "POST")
);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (requestMatcher.matches(request)) {
//将自定义参数,保存到当前本地线程中
MyParamValue paramValue = new MyParamValue();
paramValue.setAuthParameters(request.getParameterMap());
MyParamValueThreadLocal.set(paramValue);
filterChain.doFilter(request, response);
//执行完成,清除线程本地变量
MyParamValueThreadLocal.remove();
} else {
filterChain.doFilter(request, response);
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
自定义异常信息返回值MyWebResponseExceptionTranslator
这里的响应码一定要设置为200,若取oauth2返回的非200响应码,在微服务调用过程中,返回值无法被正常序列化
return new ResponseEntity<>(
ExceptionResponse.fail(status,
e.getMessage())
, headers,
HttpStatus.OK);
crm调用auth服务的feign接口
package pers.kw.config.oauth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.DefaultThrowableAnalyzer;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import pers.kw.protocol.ExceptionResponse;
import java.io.IOException;
public class MyWebResponseExceptionTranslator implements WebResponseExceptionTranslator {
private static final Logger log = LoggerFactory.getLogger(MyWebResponseExceptionTranslator.class);
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
@Override
public ResponseEntity<ExceptionResponse> translate(Exception e) throws Exception {
log.error("OAuth2异常处理:", e);
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e);
Exception ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain);
if (ase != null) {
if (ase instanceof InvalidGrantException) {
log.info("ase:{}", ase.getMessage());
return handleOAuth2Exception((OAuth2Exception) ase, "密码错误");
}
return handleOAuth2Exception((OAuth2Exception) ase);
}
ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class,
causeChain);
if (ase != null) {
return handleOAuth2Exception(new UnauthorizedException(e.getMessage(), e));
}
ase = (AccessDeniedException) throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
if (ase != null) {
return handleOAuth2Exception(new ForbiddenException(ase.getMessage(), ase));
}
ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer.getFirstThrowableOfType(
HttpRequestMethodNotSupportedException.class, causeChain);
if (ase != null) {
return handleOAuth2Exception(new MethodNotAllowed(ase.getMessage(), ase));
}
return handleOAuth2Exception(new ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e));
}
private ResponseEntity<ExceptionResponse> handleOAuth2Exception(OAuth2Exception e, String msg) throws IOException {
int status = e.getHttpErrorCode();
HttpHeaders headers = new HttpHeaders();
headers.set("Cache-Control", "no-store");
headers.set("Pragma", "no-cache");
if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) {
headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
}
//HttpStatus.valueOf(status)
return new ResponseEntity<>(
ExceptionResponse.fail(status,
msg)
, headers, HttpStatus.OK
);
}
private ResponseEntity<ExceptionResponse> handleOAuth2Exception(OAuth2Exception e) throws IOException {
int status = e.getHttpErrorCode();
HttpHeaders headers = new HttpHeaders();
headers.set("Cache-Control", "no-store");
headers.set("Pragma", "no-cache");
if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) {
headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
}
return new ResponseEntity<>(
ExceptionResponse.fail(status,
e.getMessage())
, headers,
HttpStatus.OK);
}
public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
this.throwableAnalyzer = throwableAnalyzer;
}
private static class ForbiddenException extends OAuth2Exception {
public ForbiddenException(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "access_denied";
}
@Override
public int getHttpErrorCode() {
return 403;
}
}
private static class ServerErrorException extends OAuth2Exception {
public ServerErrorException(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "server_error";
}
@Override
public int getHttpErrorCode() {
return 500;
}
}
private static class UnauthorizedException extends OAuth2Exception {
public UnauthorizedException(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "unauthorized";
}
@Override
public int getHttpErrorCode() {
return 401;
}
}
private static class MethodNotAllowed extends OAuth2Exception {
public MethodNotAllowed(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "method_not_allowed";
}
@Override
public int getHttpErrorCode() {
return 405;
}
}
}