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);@Overridepublic 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"));}@Overridepublic 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);}}@Overridepublic 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();@Overridepublic ResponseEntity<ExceptionResponse> translate(Exception e) throws Exception {log.error("OAuth2异常处理:", e);// Try to extract a SpringSecurityException from the stacktraceThrowable[] 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);}@Overridepublic String getOAuth2ErrorCode() {return "access_denied";}@Overridepublic int getHttpErrorCode() {return 403;}}private static class ServerErrorException extends OAuth2Exception {public ServerErrorException(String msg, Throwable t) {super(msg, t);}@Overridepublic String getOAuth2ErrorCode() {return "server_error";}@Overridepublic int getHttpErrorCode() {return 500;}}private static class UnauthorizedException extends OAuth2Exception {public UnauthorizedException(String msg, Throwable t) {super(msg, t);}@Overridepublic String getOAuth2ErrorCode() {return "unauthorized";}@Overridepublic int getHttpErrorCode() {return 401;}}private static class MethodNotAllowed extends OAuth2Exception {public MethodNotAllowed(String msg, Throwable t) {super(msg, t);}@Overridepublic String getOAuth2ErrorCode() {return "method_not_allowed";}@Overridepublic int getHttpErrorCode() {return 405;}}
}