基于OAuth2授权如何拓展出自己的认证方式?
查看该博客我们默认读者已经清楚OAuth的认证流程以及AuthenticationManager,BearerTokenExtractor,OAuth2AuthenticationEntryPoint,各个类的原理和使用方式。
场景:例如,外部客户端或服务端如何使用不固定的参数登录到本系统?
比如当前我们的系统已经集成好了OAuth2协议,但是某些场景在给客户端或服务端提供对外接口时,这些接口的保护授权方式都是独立的,accessToken也是不会在head中的bearer中传进来的,这时候就需要我们额外扩展一种授权方式去兼容这些客户端或服务端的。
开始扩展
-
第一步,实现基于OAuth2认证管理器自己的认证管理器
创建自己的认证管理器,并继承OAuth2默认的认证管理器(OAuth2AuthenticationManager),并实现authenticate认证方法例如自己的类叫AliyunAuthenticationManager,相关实现: -
@Autowired private AliyunAuthenticationService authenticationService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { if (authentication instanceof AliyunAuthentication) { AliyunAuthentication aliyunAuthentication = (AliyunAuthentication) authentication; AliyunAuthenticationDetails authenticationDetails = (AliyunAuthenticationDetails) aliyunAuthentication.getDetails(); return authenticationService.authentication(authentication, authenticationDetails); } Authentication auth = super.authenticate(authentication); if (auth instanceof OAuth2Authentication) { OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) auth; if (oAuth2Authentication.getOAuth2Request() != null) { checkOAuth2Request(oAuth2Authentication.getOAuth2Request()); } } return auth; } private void checkOAuth2Request(OAuth2Request oauth2Request) { Map<String, Serializable> extensions = oauth2Request.getExtensions(); if (extensions != null) { if (extensions.containsKey(REMOTE_HOST)) { Serializable host = extensions.get(REMOTE_HOST); final String clientIp = WebUtils.getIp(); if (host != null && !host.equals(clientIp)) { LOG.warn("[{}]- Invalid token, Ip mismatch, host: {}, client-ip: {}, oauth2Request: {}", RIDHolder.id(), host, clientIp, oauth2Request); // throw new AliyunAuthenticationException(HttpStatus.UNAUTHORIZED.value(), "Invalid token ,Ip mismatch [" + host + "], please login again", RIDHolder.id()); } } } }
-
第二步,定义自己的认证对象并实现Authentication认证接口,例如自己的认证对象叫AliyunAuthentication,相关实现:
-
private static final long serialVersionUID = -2705852848220228058L; private boolean authenticated; private AliyunAuthenticationDetails detail; private Collection<? extends GrantedAuthority> authorities = new ArrayList<>(); public AliyunAuthentication(AliyunAuthenticationDetails detail) { this.detail = detail; this.authenticated = false; } public AliyunAuthentication(AliyunAuthenticationDetails detail, Object user, Collection<? extends GrantedAuthority> authorities) { this.detail = detail; this.authenticated = true; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public Object getCredentials() { return detail == null ? null : detail.getClusterSign(); } @Override public Object getDetails() { return detail; } @Override public Object getPrincipal() { return authenticated ? "" : (detail == null ? null : detail.getClusterUuid()); } @Override public boolean isAuthenticated() { return authenticated; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { throw new IllegalArgumentException(""); } @Override public String getName() { return null; }
-
第三步,定义自己的accessToken提取机制
原来传递accessToken在head的bearer中参数名也是固定,现在的新增一种你自己的提取方式参数也是你自己定义的,自己定义的类叫AliyunTokenExtractor并继承默认accessToken提取类BearerTokenExtractor,我要提取自己定义的一些参数,相关实现如下: -
@Override public Authentication extract(HttpServletRequest request) { Authentication authentication = doExtract(request); if (authentication != null) { return authentication; } return super.extract(request); } private Authentication doExtract(HttpServletRequest request) { String clusterUuid = request.getParameter("externalId"); String clusterKey = request.getParameter("externalIdKey"); if (StringUtils.isNotBlank(clusterUuid) && StringUtils.isNotBlank(clusterKey)) { String clusterSign = request.getParameter("externalIdSign"); String clusterTime = request.getParameter("externalIdTime"); String requestId = request.getParameter("requestId"); String instanceId = request.getParameter("externalIdId"); String aliUid = request.getParameter("externalIdUid"); LOG.debug("[{}]- Match aliyun authentication", RIDHolder.id()); AliyunAuthenticationDetails details = new AliyunAuthenticationDetails(clusterUuid, clusterKey, clusterTime, clusterSign, requestId); details.setAliUid(aliUid); details.setInstanceId(instanceId); details.setRequestURI(request.getRequestURI()); return new AliyunAuthentication(details); } else { LOG.debug("[{}]- Match common authentication", RIDHolder.id()); } return null; }
-
第四步,定义属于自己的异常类
该步骤是为了当匹配到是自己定义的参数,但是参数不合法或请求不合法时抛出自己的OAuth异常,实现方式:定义自己的异常类(AliyunAuthenticationException)并继承OAuth默认异常类(OAuth2Exception):
-
// http status private int code; private String message; private String requestId; public AliyunAuthenticationException(int code, String message, String requestId) { super(message); this.code = code; this.message = message; this.requestId = requestId; } public int getCode() { return code; } public int getHttpErrorCode() { return this.code; } @Override public String getMessage() { return message; } public String getRequestId() { return requestId; }
-
第五步,定义属于自己的异常捕获类
当我们抛出自己定义的异常类时是需要捕获的,捕获的逻辑下面会讲,我们先定义自己的捕获类,AliyunAuthenticationEntryPoint,并继承OAuth默认异常捕获类:OAuth2AuthenticationEntryPoint,相关实现:
-
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { AliyunAuthenticationException exception = (AliyunAuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AliyunAuthenticationException.class, throwableAnalyzer.determineCauseChain(authException)); if (exception != null) { LOG.warn("[{}]- external authentication failed ,Code:{},Message:{}", RIDHolder.id(), exception.getCode(), exception.getMessage()); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(buildJSONResponse(exception)); return; } super.commence(request, response, authException); } private String buildJSONResponse(AliyunAuthenticationException exception) { JSONObject json = new JSONObject(); json.put("success", false); json.put("code", exception.getCode()); json.put("message", exception.getMessage()); json.put("id", exception.getRequestId()); return json.toString(); }
-
第六步,定义自己的service和实现
实现自己的service和impl,如果有问题则抛出自定义的异常,否则认证成功返回oauth用户信息,相关AliyunAuthenticationService方法authentication实现部分示例:
-
if (authenticationDetails.getClusterTime() == null || !StringUtils.isNumeric(authenticationDetails.getClusterTime())) { throw new AliyunAuthenticationException(BAD_REQUEST.value(), MessageUtils.getMessage("aliyun.authentication.err.cluster.time.is.null", "时间戳数据为空或者不是数字"), authenticationDetails.getRequestId()); } if (authenticationDetails.getClusterUuid() == null || !authenticationDetails.getClusterUuid().equals(configurationHolder.getClusterUuid())) { throw new AliyunAuthenticationException(BAD_REQUEST.value(), MessageUtils.getMessage("aliyun.authentication.err.cluster.uuid.is.null", "ClusterUuid参数为空或者不匹配期望[" + configurationHolder.getClusterUuid() + "],实际[" + authenticationDetails.getClusterUuid() + "]", configurationHolder.getClusterUuid(), authenticationDetails.getClusterUuid()), authenticationDetails.getRequestId()); } if (authenticationDetails.getClusterKey() == null || !authenticationDetails.getClusterKey().equals(configurationHolder.getClusterKey())) { throw new AliyunAuthenticationException(BAD_REQUEST.value(), MessageUtils.getMessage("aliyun.authentication.err.cluster.key.is.null", "ClusterKey参数为空或者不匹配期望[" + configurationHolder.getClusterKey() + "],实际[" + authenticationDetails.getClusterKey() + "]", configurationHolder.getClusterKey(), authenticationDetails.getClusterKey()), authenticationDetails.getRequestId()); } //验证当前请求地址是否是自己需要特殊拦截的,如果是则初始化new 一个自己定义的用户details,示例: UserDetails details; if (authenticationDetails.getRequestURI() != null && authenticationDetails.getRequestURI().startsWith("/api/bff/" + API_VERSION + "/external")) { details = new AliyunIdsUserDetails(); } //你自己的认证方法,验证当前参数或用户信息,如果成功则返回用户usernamepasswordToken对象 //返回用户登录成功信息 UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( details, authentication.getCredentials(), details.getAuthorities()); result.setDetails(authenticationDetails); return result;
-
第七步,初始化bean,将自己定义的service和bean定义到security.xml
初始化bean,定义自己实现的token提取类和自定义的认证管理器到security.xml中并引用tokenService实现类,tokenService是你默认已经实现集成好的那个accessToken service实现类,该步骤用于创建,获取或刷新accessToken使用,示例:
-
<beans:bean id="aliyunTokenExtractor" class="com.idsmanager.bff.service.business.aliyun.AliyunTokenExtractor"/> <beans:bean id="aliyunAuthenticationManager" class="com.idsmanager.bff.service.business.aliyun.AliyunAuthenticationManager"> <beans:property name="resourceId" value="bff_api_resource"/> <beans:property name="tokenServices" ref="tokenServices"/> </beans:bean> <beans:bean id="aliyunAuthenticationEntryPoint" class="com.idsmanager.bff.service.business.aliyun.AliyunAuthenticationEntryPoint"/> <oauth2:resource-server id="bffApiResourceServer" resource-id="bff_api_resource" token-services-ref="tokenServices" token-extractor-ref="aliyunTokenExtractor" authentication-manager-ref="aliyunAuthenticationManager" entry-point-ref="aliyunAuthenticationEntryPoint" />
-
最后一步,定义自己想拦截的API或后台URL地址
定义自己的API,以及这些API的命名都是需要以上权限或参数才能有权限访问的到,例如:/api/bff/v1.2/external/** ,当客户端访问该地址开头时将走自定义的认证器,token提取器,以及异常处理器,认证成功则成功,失败则抛出自定义异常,配置示例:
-
<http pattern="/api/bff/v1.2/**" create-session="never" entry-point-ref="oauth2AuthenticationEntryPoint" access-decision-manager-ref="oauth2AccessDecisionManager" use-expressions="false"> <!--OPS 模块--> <intercept-url pattern="/api/bff/v1.2/external/**" method="GET" access="ROLE_ALIYUN_OPS_API,SCOPE_READ"/> <intercept-url pattern="/api/bff/v1.2/external/**" method="POST" access="ROLE_ALIYUN_OPS_API,SCOPE_READ"/> <intercept-url pattern="/api/bff/v1.2/external/**" method="PUT" access="ROLE_ALIYUN_OPS_API,SCOPE_READ"/> <intercept-url pattern="/api/bff/v1.2/external/**" method="DELETE" access="ROLE_ALIYUN_OPS_API,SCOPE_READ"/> <custom-filter ref="bffApiResourceServer" before="PRE_AUTH_FILTER"/> <access-denied-handler ref="oauth2AccessDeniedHandler"/> <csrf disabled="true"/> </http>
到这里基本配置和实现流程基本完毕,有些细节不方便透露大概就是这个流程,有问题可以进行沟通,编写不易点个赞吧~~~