授权码模式使用是最广泛,而且大多数互联网站点都使用了此模式,如第三方使用QQ和微信登录。
一、Spring Security OAuth2表结构
授权码模式用到了四张表,这里贴上Oracle版本的脚本。
-- OAUTH2.0 需要的4张表
CREATE TABLE OAUTH_CLIENT_DETAILS
(
CLIENT_ID VARCHAR2(128) NOT NULL
CONSTRAINT PK_OAUTH_CLIENT_DETAILS
PRIMARY KEY,
RESOURCE_IDS VARCHAR2(128) DEFAULT NULL,
CLIENT_SECRET VARCHAR2(128) DEFAULT NULL,
SCOPE VARCHAR2(128) DEFAULT NULL,
AUTHORIZED_GRANT_TYPES VARCHAR2(128) DEFAULT NULL,
WEB_SERVER_REDIRECT_URI VARCHAR2(1024) DEFAULT NULL,
AUTHORITIES VARCHAR2(128) DEFAULT NULL,
ACCESS_TOKEN_VALIDITY NUMBER(11) DEFAULT NULL,
REFRESH_TOKEN_VALIDITY NUMBER(11) DEFAULT NULL,
ADDITIONAL_INFORMATION VARCHAR2(4000) DEFAULT NULL,
AUTOAPPROVE VARCHAR2(128) DEFAULT NULL
);
COMMENT ON TABLE OAUTH_CLIENT_DETAILS IS '应用表';
COMMENT ON COLUMN OAUTH_CLIENT_DETAILS.CLIENT_ID IS '应用ID';
COMMENT ON COLUMN OAUTH_CLIENT_DETAILS.RESOURCE_IDS IS '授权资源ID集合';
COMMENT ON COLUMN OAUTH_CLIENT_DETAILS.CLIENT_SECRET IS '应用密钥';
COMMENT ON COLUMN OAUTH_CLIENT_DETAILS.SCOPE IS '授权作用域';
COMMENT ON COLUMN OAUTH_CLIENT_DETAILS.AUTHORIZED_GRANT_TYPES IS '授权允许类型';
COMMENT ON COLUMN OAUTH_CLIENT_DETAILS.WEB_SERVER_REDIRECT_URI IS '授权回调地址';
COMMENT ON COLUMN OAUTH_CLIENT_DETAILS.AUTHORITIES IS '拥有权限集合';
COMMENT ON COLUMN OAUTH_CLIENT_DETAILS.ACCESS_TOKEN_VALIDITY IS 'ACCESS_TOKEN有效期(秒)';
COMMENT ON COLUMN OAUTH_CLIENT_DETAILS.REFRESH_TOKEN_VALIDITY IS 'REFRESH_TOKEN有效期(秒)';
COMMENT ON COLUMN OAUTH_CLIENT_DETAILS.ADDITIONAL_INFORMATION IS '附加信息(预留)';
COMMENT ON COLUMN OAUTH_CLIENT_DETAILS.AUTOAPPROVE IS '自动同意授权(TRUE,FALSE,READ,WRITE)';
CREATE TABLE OAUTH_CODE
(
CODE VARCHAR2(128) DEFAULT NULL,
AUTHENTICATION BLOB
);
CREATE INDEX IX_OAUTH_CODE_CODE ON OAUTH_CODE (CODE);
COMMENT ON TABLE OAUTH_CODE IS '授权码表';
COMMENT ON COLUMN OAUTH_CODE.CODE IS '授权码';
COMMENT ON COLUMN OAUTH_CODE.AUTHENTICATION IS '身份验证信息';
CREATE TABLE OAUTH_ACCESS_TOKEN
(
AUTHENTICATION_ID VARCHAR2(128) NOT NULL
CONSTRAINT PK_OAUTH_ACCESS_TOKEN
PRIMARY KEY,
TOKEN_ID VARCHAR2(128) DEFAULT NULL,
TOKEN BLOB,
USER_NAME VARCHAR2(128) DEFAULT NULL,
CLIENT_ID VARCHAR2(128) DEFAULT NULL,
AUTHENTICATION BLOB,
REFRESH_TOKEN VARCHAR2(128) DEFAULT NULL
);
CREATE INDEX IX_OAT_TOKEN_ID ON OAUTH_ACCESS_TOKEN (TOKEN_ID);
CREATE INDEX IX_OAT_USER_NAME ON OAUTH_ACCESS_TOKEN (USER_NAME);
CREATE INDEX IX_OAT_CLIENT_ID ON OAUTH_ACCESS_TOKEN (CLIENT_ID);
CREATE INDEX IX_OAT_REFRESH_TOKEN ON OAUTH_ACCESS_TOKEN (REFRESH_TOKEN);
COMMENT ON TABLE OAUTH_ACCESS_TOKEN IS '授权TOKEN表';
COMMENT ON COLUMN OAUTH_ACCESS_TOKEN.AUTHENTICATION_ID IS '身份验证ID';
COMMENT ON COLUMN OAUTH_ACCESS_TOKEN.TOKEN_ID IS 'ACCESS_TOKEN加密值';
COMMENT ON COLUMN OAUTH_ACCESS_TOKEN.TOKEN IS 'ACCESS_TOKEN真实值';
COMMENT ON COLUMN OAUTH_ACCESS_TOKEN.USER_NAME IS '用户名';
COMMENT ON COLUMN OAUTH_ACCESS_TOKEN.CLIENT_ID IS '应用ID';
COMMENT ON COLUMN OAUTH_ACCESS_TOKEN.AUTHENTICATION IS '身份验证信息';
COMMENT ON COLUMN OAUTH_ACCESS_TOKEN.REFRESH_TOKEN IS 'REFRESH_TOKEN加密值';
CREATE TABLE OAUTH_REFRESH_TOKEN
(
TOKEN_ID VARCHAR2(128) DEFAULT NULL,
TOKEN BLOB,
AUTHENTICATION BLOB
);
CREATE INDEX IX_ORT_TOKEN_ID ON OAUTH_REFRESH_TOKEN (TOKEN_ID);
COMMENT ON TABLE OAUTH_REFRESH_TOKEN IS '刷新TOKEN表';
COMMENT ON COLUMN OAUTH_REFRESH_TOKEN.TOKEN_ID IS 'ACCESS_TOKEN加密值';
COMMENT ON COLUMN OAUTH_REFRESH_TOKEN.TOKEN IS 'ACCESS_TOKEN真实值';
COMMENT ON COLUMN OAUTH_REFRESH_TOKEN.AUTHENTICATION IS '身份验证信息';
二、服务端实现
1、Maven引用
springboot
org.springframework.boot
spring-boot-starter-parent
2.1.9.RELEASE
spring security
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth
spring-security-oauth2
2.3.6.RELEASE
2、配置Security
WebSecurityConfiguration.java
package com.leven.platform.auth.security;
import com.leven.commons.core.util.BeanMapper;
import com.leven.commons.model.exception.BasicEcode;
import com.leven.platform.auth.security.custom.CustomAuthenticationFilter;
import com.leven.platform.auth.security.custom.CustomAuthenticationProvider;
import com.leven.platform.auth.security.custom.CustomResponse;
import com.leven.platform.model.constant.PlatformConstant;
import com.leven.platform.model.constant.PlatformExceptionCode;
import com.leven.platform.model.pojo.dto.PlatformUserDTO;
import com.leven.platform.model.pojo.security.PlatformUserDetails;
import com.leven.platform.service.properties.WebProperties;
import com.leven.platform.service.security.encoder.ClientSecretEncoder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import java.util.HashMap;
import java.util.Map;
/**
* spring security核心配置
*
* @author Leven
*/
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(2)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* 用户登录的图形验证码
*/
public static final String USER_LOGIN_VERIFY_CODE_KEY = "user_login_verify_code";
/**
* 用户登录的图形验证码过期时间
*/
public static final String USER_LOGIN_VERIFY_CODE_EXPIRED_KEY = "user_login_verify_code_expired";
@Autowired
private WebProperties webProperties;
@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
Map idToPasswordEncoder = new HashMap<>();
idToPasswordEncoder.put(PlatformConstant.ID_FOR_ENCODE_DEFAULT, new BCryptPasswordEncoder());
idToPasswordEncoder.put(ClientSecretEncoder.ID_FOR_ENCODE, new ClientSecretEncoder());
return new DelegatingPasswordEncoder(PlatformConstant.ID_FOR_ENCODE_DEFAULT, idToPasswordEncoder);
}
/**
* 注册自定义的UsernamePasswordAuthenticationFilter
*
* @return
* @throws Exception
*/
@Bean
public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
// 认证成功处理
filter.setAuthenticationSuccessHandler((request, response, authentication) -> {
PlatformUserDetails principal = (PlatformUserDetails) authentication.getPrincipal();
log.info("用户[{}]登录成功!", principal.getTrueName());
PlatformUserDTO platformUserDTO = BeanMapper.map(principal, PlatformUserDTO.class);
// 如果会话中有缓存请求,前端登录成功后应重定向到原地址
RequestCache cache = new HttpSessionRequestCache();
SavedRequest savedRequest = cache.getRequest(request, response);
if (savedRequest != null) {
String url = savedRequest.getRedirectUrl();
if (StringUtils.isNotBlank(url)) {
platformUserDTO.setRedirectUrl(url);
}
}
CustomResponse.success(response, platformUserDTO);
});
// 认证失败处理
filter.setAuthenticationFailureHandler((request, response, exception) -> {
log.info("登录失败:", exception);
if (exception instanceof UsernameNotFoundException) {
CustomResponse.error(response, PlatformExceptionCode.USERNAME_NOT_FOUND);
} else if (exception instanceof DisabledException) {
CustomResponse.error(response, PlatformExceptionCode.ACCOUNT_DISABLED);
} else if (exception instanceof AccountExpiredException) {
CustomResponse.error(response, PlatformExceptionCode.ACCOUNT_EXPIRED);
} else if (exception instanceof LockedException) {
CustomResponse.error(response, PlatformExceptionCode.ACCOUNT_LOCKED);
} else if (exception instanceof CredentialsExpiredException) {
CustomResponse.error(response, PlatformExceptionCode.CREDENTIALS_EXPIRED);
} else if (exception instanceof AuthenticationServiceException) {
CustomResponse.error(response, PlatformExceptionCode.AUTHENTICATION_ERROR);
} else if (exception instanceof BadCredentialsException) {
CustomResponse.error(response, PlatformExceptionCode.BAD_CREDENTIALS);
} else if (exception instanceof AuthenticationCredentialsNotFoundException) {
CustomResponse.error(response, PlatformExceptionCode.CHECK_CA_ERROR, exception.getMessage());
} else {
CustomResponse.error(response, BasicEcode.USER_ERR_UNLOGINED);
}
});
// 登录处理url
filter.setFilterProcessesUrl("/user/login");
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(customAuthenticationProvider);
}
/**
* 设置不需要拦截的静态资源
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/static/**");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers().antMatchers("/user/**", "/admin/**", "/oauth/authorize")
.and().authorizeRequests()
// 不需要拦截的请求
.antMatchers("/user/login", "/user/verifyCode",
"/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**").permitAll()
// 需要登录才能访问
.antMatchers("/user/**").hasRole("USER")
// 需要管理员才能访问
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and().formLogin().loginPage(webProperties.getLoginFormUrl()).permitAll()
.and().logout().logoutUrl("/user/logout").logoutSuccessHandler((request, response, auth) -> {
CustomResponse.success(response, null);
})
.and().exceptionHandling().authenticationEntryPoint((request, response, authException) ->
CustomResponse.error(response, BasicEcode.USER_ERR_UNLOGINED)
)
// 没有权限,返回异常信息
.accessDeniedHandler((request, response, authException) ->
CustomResponse.error(response, BasicEcode.PERMISSION_DENIED)
)
// CSRF防护:与前端联调存在跨域,需要禁用 csrf().disable()
.and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// 添加自定义授权认证拦截器
http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
WebProperties.java
package com.leven.platform.service.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* web配置
*/
@Component
@ConfigurationProperties(prefix = "web")
@Data
public class WebProperties {
/**
* 用户登陆地址
*/
private String loginFormUrl;
/**
* 启用图形验证码
*/
private Boolean enableVerifyCode;
}
CustomAuthenticationProvider.java
package com.leven.platform.auth.security.custom;
import com.leven.platform.model.constant.PlatformExceptionCode;
import com.leven.platform.model.pojo.security.PlatformUserDetails;
import com.leven.platform.service.PlatformUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 自定义认证服务实现
* @author Leven
*/
@Slf4j
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private PlatformUserService platformUserService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
String username = token.getName();
AuthenticationBean loginBean = (AuthenticationBean) token.getCredentials();
PlatformUserDetails userDetails = null;
if (username != null) {
userDetails = (PlatformUserDetails) platformUserService.loadUserByUsername(username);
}
if (userDetails == null) {
throw new UsernameNotFoundException(PlatformExceptionCode.USERNAME_NOT_FOUND);
} else if (!userDetails.isEnabled()) {
throw new DisabledException(PlatformExceptionCode.ACCOUNT_DISABLED);
} else if (!userDetails.isAccountNonExpired()) {
throw new AccountExpiredException(PlatformExceptionCode.ACCOUNT_EXPIRED);
} else if (!userDetails.isAccountNonLocked()) {
throw new LockedException(PlatformExceptionCode.ACCOUNT_LOCKED);
} else if (!userDetails.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(PlatformExceptionCode.CREDENTIALS_EXPIRED);
}
// 校验密码
checkPwd(loginBean, userDetails);
// 授权
return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
}
@Override
public boolean supports(Class> aClass) {
// 返回true后才会执行上面的authenticate方法,这步能确保authentication能正确转换类型
return UsernamePasswordAuthenticationToken.class.equals(aClass);
}
/**
* 密码验证
* @param loginBean
* @param userDetails
*/
private void checkPwd(AuthenticationBean loginBean, PlatformUserDetails userDetails) {
Date pwdExpired = userDetails.getPwdExpired();
// 用户输入的明文
String plainPwd = loginBean.getPassword();
// 数据库存储的密文
String password = userDetails.getPassword();
if (System.currentTimeMillis() > pwdExpired.getTime()) {
throw new CredentialsExpiredException(PlatformExceptionCode.CREDENTIALS_EXPIRED);
}
// 校验密码是否正确
if (!passwordEncoder.matches(plainPwd, password)) {
throw new BadCredentialsException(PlatformExceptionCode.BAD_CREDENTIALS);
}
}
}
PlatformUserService.java
package com.leven.platform.service;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* 用户信息接口
* @author Leven
* @date 2019-05-20
*/
public interface PlatformUserService extends UserDetailsService {
}
PlatformUserServiceImpl.java
package com.leven.platform.service.impl;
import com.leven.platform.core.mapper.PlatformUserMapper;
import com.leven.platform.model.enums.UsedEnum;
import com.leven.platform.model.pojo.dto.PlatformUserDTO;
import com.leven.platform.model.pojo.query.PlatformUserQuery;
import com.leven.platform.model.pojo.security.PlatformUserDetails;
import com.leven.platform.service.PlatformUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* 用户信息接口实现
* @author Leven
* @date 2019-05-20
*/
@Slf4j
@Service
public class PlatformUserServiceImpl implements PlatformUserService {
@Resource
private PlatformUserMapper platformUserMapper;
@Override
public PlatformUserDetails loadUserByUsername(String username) {
PlatformUserQuery query = new PlatformUserQuery();
query.setUsername(username);
PlatformUserDTO userDTO = platformUserMapper.getDTOByQuery(query);
if (userDTO == null) {
throw new UsernameNotFoundException("Could not find the user '" + username + "'");
}
boolean enabled = UsedEnum.ENABLE.getValue().equals(userDTO.getUsed());
// 设置用户拥有的角色
List grantedAuthorities = AuthorityUtils.createAuthorityList(platformUserMapper.listRoles(userDTO.getOpenid()));
return new PlatformUserDetails(userDTO, enabled, true, true,
true, grantedAuthorities);
}
}
CustomResponse.java
package com.leven.platform.auth.security.custom;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.leven.commons.core.web.bean.OuterResult;
import com.leven.commons.model.exception.BasicEcode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义响应JSON
* @author Leven
*/
@Slf4j
public class CustomResponse {
private static final String CONTENT_TYPE = "application/json;charset=UTF-8";
private CustomResponse(){}
/**
* 成功返回结果
* @param response
* @param obj
* @throws IOException
*/
public static void success(HttpServletResponse response, Object obj) {
print(response, BasicEcode.SUCCESS, null, obj);
}
/**
* 异常返回结果
* @param response
* @param ecode
* @throws IOException
*/
public static void error(HttpServletResponse response, String ecode) {
error(response, ecode, BasicEcode.getMsg(ecode));
}
/**
* 异常返回结果
* @param response
* @param ecode
* @throws IOException
*/
public static void error(HttpServletResponse response, String ecode, String msg) {
print(response, ecode, msg, null);
}
/**
* 异常返回结果
* @param response
* @param ecode
* @throws IOException
*/
public static void error(HttpServletResponse response, String ecode, Object... args) {
print(response, ecode, null, null, args);
}
private static void print(HttpServletResponse response, String ecode, String msg, Object data, Object... args) {
OuterResult result = OuterResult.newInstance();
result.setEcode(ecode);
if (StringUtils.isBlank(msg)) {
msg = BasicEcode.getMsg(ecode);
}
if (args != null && args.length > 0) {
msg = String.format(msg, args);
}
result.setMsg(msg);
result.setData(data);
response.setContentType(CONTENT_TYPE);
try {
response.getWriter().print(JSON.toJSONString(result, SerializerFeature.WriteMapNullValue));
} catch (IOException e) {
log.error("打印返回结果报错:", e);
}
}
}
AuthenticationBean.java
package com.leven.platform.auth.security.custom;
import com.leven.commons.model.pojo.BaseDTO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
/**
* 登录认证bean
* @author Leven
*/
@ApiModel("登录认证bean")
@Getter
@Setter
public class AuthenticationBean extends BaseDTO {
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("密码")
private String password;
@ApiModelProperty("验证码")
private String verifyCode;
}
CustomAuthenticationFilter.java
package com.leven.platform.auth.security.custom;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.leven.platform.auth.security.WebSecurityConfiguration;
import com.leven.platform.model.constant.PlatformConstant;
import com.leven.platform.model.constant.PlatformExceptionCode;
import com.leven.platform.service.properties.WebProperties;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.InputStream;
/**
* 自定义认证过滤器
* 为了实现JSON方式进行用户登录
*
* @author Leven
*/
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
private WebProperties webProperties;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
// 不是POST提交直接报错
if (!PlatformConstant.POST.equalsIgnoreCase(request.getMethod())) {
CustomResponse.error(response, PlatformExceptionCode.UNSUPPORTED_REQUEST_METHOD);
return null;
}
String contentType = request.getContentType();
// 不是JSON提交直接报错
if (!contentType.equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)
&& !contentType.equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
CustomResponse.error(response, PlatformExceptionCode.UNSUPPORTED_CONTENT_TYPE);
return null;
}
ObjectMapper mapper = new ObjectMapper();
UsernamePasswordAuthenticationToken authRequest;
try (InputStream is = request.getInputStream()) {
AuthenticationBean authenticationBean = mapper.readValue(is, AuthenticationBean.class);
String username = authenticationBean.getUsername();
String password = authenticationBean.getPassword();
String verifyCode = authenticationBean.getVerifyCode();
if (StringUtils.isBlank(username)) {
CustomResponse.error(response, PlatformExceptionCode.USERNAME_IS_BLANK);
return null;
}
if (StringUtils.isBlank(password)) {
CustomResponse.error(response, PlatformExceptionCode.PASSWORD_IS_BLANK);
return null;
}
if (webProperties.getEnableVerifyCode()) {
// 校验图形验证码
HttpSession session = request.getSession();
String sessionCode = (String) session.getAttribute(WebSecurityConfiguration.USER_LOGIN_VERIFY_CODE_KEY);
Long verifyCodeExpired = (Long) session.getAttribute(WebSecurityConfiguration.USER_LOGIN_VERIFY_CODE_EXPIRED_KEY);
if (StringUtils.isBlank(sessionCode) || verifyCodeExpired == null) {
CustomResponse.error(response, PlatformExceptionCode.VERIFY_CODE_EXPIRED);
return null;
}
if (System.currentTimeMillis() > verifyCodeExpired) {
CustomResponse.error(response, PlatformExceptionCode.VERIFY_CODE_EXPIRED);
return null;
}
if (!sessionCode.equalsIgnoreCase(verifyCode)) {
CustomResponse.error(response, PlatformExceptionCode.VERIFY_CODE_ERROR);
return null;
}
// 图形验证码校验成功后,直接从会话中移除
session.removeAttribute(WebSecurityConfiguration.USER_LOGIN_VERIFY_CODE_KEY);
session.removeAttribute(WebSecurityConfiguration.USER_LOGIN_VERIFY_CODE_EXPIRED_KEY);
}
authRequest = new UsernamePasswordAuthenticationToken(
authenticationBean.getUsername(), authenticationBean);
} catch (IOException e) {
logger.error("自定义认证出现异常:", e);
CustomResponse.error(response, PlatformExceptionCode.USER_LOGIN_ERROR, e.getMessage());
return null;
}
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
3、配置授权服务
AuthorizationServerConfiguration.java
package com.leven.platform.auth.security;
import com.leven.platform.auth.security.custom.CustomTokenEnhancer;
import com.leven.platform.auth.security.custom.CustomWebResponseExceptionTranslator;
import com.leven.platform.service.PlatformUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
/**
* Oauth2.0 认证授权服务配置
* @author Leven
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private PlatformUserService platformUserService;
/**
* 声明TokenStore实现
* @return
*/
@Bean("tokenStore")
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
/**
* 声明 ClientDetails实现
* @return
*/
@Bean
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
@Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());
}
@SuppressWarnings("unchecked")
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore())
.authorizationCodeServices(authorizationCodeServices());
endpoints.tokenEnhancer(tokenEnhancer());
endpoints.userDetailsService(platformUserService);
endpoints.exceptionTranslator(new CustomWebResponseExceptionTranslator());
endpoints.setClientDetailsService(clientDetailsService());
}
}
CustomTokenEnhancer.java
package com.leven.platform.auth.security.custom;
import com.leven.platform.model.pojo.security.PlatformUserDetails;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义Token增强
* @author Leven
*/
public class CustomTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
if (accessToken instanceof DefaultOAuth2AccessToken) {
DefaultOAuth2AccessToken token = ((DefaultOAuth2AccessToken) accessToken);
Map additionalInformation = new HashMap<>();
PlatformUserDetails userDetails = (PlatformUserDetails) authentication.getPrincipal();
additionalInformation.put("openid", userDetails.getOpenid());
token.setAdditionalInformation(additionalInformation);
return token;
}
return accessToken;
}
}
CustomWebResponseExceptionTranslator.java
package com.leven.platform.auth.security.custom;
import com.leven.commons.core.web.bean.OuterResult;
import com.leven.commons.model.exception.BasicEcode;
import com.leven.platform.model.constant.PlatformExceptionCode;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
/**
* 自定义异常处理
* @author Leven
*/
public class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator {
@Override
public ResponseEntity translate(Exception e) {
if (e instanceof OAuth2Exception) {
OAuth2Exception exception = ((OAuth2Exception) e);
String code = exception.getOAuth2ErrorCode();
return ResponseEntity.ok(new OuterResult(code, exception.getMessage()));
}
return ResponseEntity.ok(new OuterResult(PlatformExceptionCode.OAUTH2_ERROR,
BasicEcode.getMsg(PlatformExceptionCode.OAUTH2_ERROR)));
}
}
4、配置资源服务
ResourceServerConfiguration.java
package com.leven.platform.auth.security;
import com.leven.platform.auth.security.custom.CustomAccessDeniedHandler;
import com.leven.platform.auth.security.custom.CustomOAuth2AuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;
/**
* Oauth2.0 资源服务配置
* @author Leven
*/
@Configuration
@EnableResourceServer
@Order(6)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
private CustomOAuth2AuthenticationEntryPoint oAuth2AuthenticationEntryPoint;
@Autowired
private CustomAccessDeniedHandler handler;
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/oauth/**").authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.authenticationEntryPoint(oAuth2AuthenticationEntryPoint).accessDeniedHandler(handler);
}
}
CustomOAuth2AuthenticationEntryPoint.java
package com.leven.platform.auth.security.custom;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定义授权认证异常处理
* @author Leven
*/
@Component
public class CustomOAuth2AuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
CustomResponse.error(response, HttpStatus.UNAUTHORIZED.getReasonPhrase(), e.getMessage());
}
}
CustomAccessDeniedHandler.java
package com.leven.platform.auth.security.custom;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定义权限异常处理
* @author Leven
*/
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) {
CustomResponse.error(response, HttpStatus.FORBIDDEN.getReasonPhrase(), e.getMessage());
}
}
三、SDK实现
为了方便客户端快速对接,简单地实现了一个sdk包,下面贴出一些关键代码。
1、Maven引用
org.projectlombok
lombok
commons-logging
commons-logging
1.2
commons-codec
commons-codec
org.apache.commons
commons-lang3
javax.servlet
javax.servlet-api
provided
com.alibaba
fastjson
1.2.73
2、配置部分(config)
在接入服务端前,需要在服务端新建一个应用,得到应用ID、应用密钥和消息加密密钥信息。
这里提供一个配置接口和内存实现,SDK的服务接口需要用到。
接口类PlatformConfigStorage.java
package com.leven.platform.api.config;
/**
* 配置存储接口
* @author Leven
*/
public interface PlatformConfigStorage {
/**
* 获取平台地址
* @return
*/
String getServerUrl();
/**
* 设置平台地址
* @param serverUrl
*/
void setServerUrl(String serverUrl);
/**
* 获取应用ID
* @return
*/
String getClientId();
/**
* 设置应用ID
* @param clientId
*/
void setClientId(String clientId);
/**
* 获取应用密钥
* @return
*/
String getClientSecret();
/**
* 设置应用密钥
* @param clientSecret
*/
void setClientSecret(String clientSecret);
/**
* 获取消息加密密钥
* @return
*/
String getEncodingAESKey();
/**
* 设置消息加密密钥
* @param encodingAESKey
*/
void setEncodingAESKey(String encodingAESKey);
/**
* 获取应用授权回调地址
* @return
*/
String getOauth2RedirectUri();
/**
* 设置应用授权回调地址
* @param oauth2redirectUri
*/
void setOauth2RedirectUri(String oauth2redirectUri);
}
内存实现类MemoryConfigStorage.java
package com.leven.platform.api.config;
/**
* 配置存储内存实现
* @author Leven
*/
public class MemoryConfigStorage implements PlatformConfigStorage {
/** 平台地址*/
private String serverUrl;
/** 应用ID*/
private String clientId;
/** 应用密钥*/
private String clientSecret;
/** 消息加密密钥*/
private String encodingAESKey;
/** 应用授权回调地址*/
private String oauth2RedirectUri;
/**
* 获取平台地址
* @return
*/
@Override
public String getServerUrl() {
return this.serverUrl;
}
/**
* 设置平台地址
* @param serverUrl
*/
@Override
public void setServerUrl(String serverUrl) {
this.serverUrl = serverUrl;
}
/**
* 获取应用ID
* @return
*/
@Override
public String getClientId() {
return this.clientId;
}
/**
* 设置应用ID
* @param clientId
*/
@Override
public void setClientId(String clientId) {
this.clientId = clientId;
}
/**
* 获取应用密钥
* @return
*/
@Override
public String getClientSecret() {
return this.clientSecret;
}
/**
* 设置应用密钥
* @param clientSecret
*/
@Override
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
/**
* 获取消息加密密钥
* @return
*/
@Override
public String getEncodingAESKey() {
return this.encodingAESKey;
}
/**
* 设置消息加密密钥
* @param encodingAESKey
*/
@Override
public void setEncodingAESKey(String encodingAESKey) {
this.encodingAESKey = encodingAESKey;
}
/**
* 获取应用授权回调地址
* @return
*/
@Override
public String getOauth2RedirectUri() {
return oauth2RedirectUri;
}
/**
* 设置应用授权回调地址
* @param oauth2redirectUri
*/
@Override
public void setOauth2RedirectUri(String oauth2redirectUri) {
this.oauth2RedirectUri = oauth2redirectUri;
}
}
3、请求响应部分
为了统一处理客户端调用服务端接口,这里设计成三个部分,分别为Request、Response和Client。
Request:封装资源服务接口的请求参数、请求路径和响应类型。
Response:封装服务接口返回数据。
Client:执行Request请求,返回结果。
3.1 请求接口
PlatformRequest.java
package com.leven.platform.api;
import java.util.Map;
/**
* 请求接口
* @param 响应类
* @author Leven
* @date 2019-06-05
*/
public interface PlatformRequest {
/**
* 获取API的映射路由。
*
* @return API名称
*/
String getApiMappingName();
/**
* 获取所有的Key-Value形式的文本请求参数集合。其中:
*
* - Key: 请求参数名
* - Value: 请求参数值
*
*
* @return 文本请求参数集合
*/
Map getTextParams();
/**
* 得到当前API的响应结果类型
*
* @return 响应类型
*/
Class getResponseClass();
}
3.2 响应抽象
AbstractPlatformResponse.java
package com.leven.platform.api;
import com.leven.platform.api.internal.util.StringUtils;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.Map;
/**
* 响应抽象
* @author Leven
* @date 2019-06-05
*/
@Getter
@Setter
public abstract class AbstractPlatformResponse implements Serializable {
private String ecode;
private String msg;
private String body;
private Map params;
public AbstractPlatformResponse() {
}
public boolean isSuccess() {
return PlatformEcode.SUCCESS.equals(ecode) || StringUtils.isEmpty(ecode);
}
}
3.3 执行器
AbstractPlatformClient
package com.leven.platform.api;
import com.leven.platform.api.internal.parser.json.ObjectJsonParser;
import com.leven.platform.api.internal.util.*;
import com.leven.platform.api.internal.util.codec.Base64;
import java.io.IOException;
import java.security.Security;
import java.util.HashMap;
import java.util.Map;
/**
* 客户端执行请求抽象
* @author Leven
* @date
*/
public abstract class AbstractPlatformClient implements PlatformClient {
private String serverUrl;
private String clientId;
private String format = PlatformConstants.FORMAT_JSON;
private String charset = PlatformConstants.CHARSET_UTF8;
private int connectTimeout = 3000;
private int readTimeout = 15000;
private static final String PREPARE_TIME = "prepareTime";
private static final String PREPARE_COST_TIME = "prepareCostTime";
private static final String REQUEST_TIME = "requestTime";
private static final String REQUEST_COST_TIME = "requestCostTime";
private static final String POST_COST_TIME = "postCostTime";
static {
Security.setProperty("jdk.certpath.disabledAlgorithms", "");
}
public AbstractPlatformClient(String serverUrl, String clientId) {
this.serverUrl = serverUrl;
this.clientId = clientId;
}
@Override
public T execute(PlatformRequest request) throws PlatformApiException {
PlatformParser parser = null;
if (PlatformConstants.FORMAT_JSON.equals(this.format)) {
parser = new ObjectJsonParser<>(request.getResponseClass());
}
return execute(request, parser);
}
private T execute(PlatformRequest request, PlatformParser parser)
throws PlatformApiException {
long beginTime = System.currentTimeMillis();
Map rt = doPost(request);
Map costTimeMap = new HashMap<>();
if (rt.containsKey(PREPARE_TIME)) {
costTimeMap.put(PREPARE_COST_TIME, (Long)(rt.get(PREPARE_TIME)) - beginTime);
if (rt.containsKey(REQUEST_TIME)) {
costTimeMap.put(REQUEST_COST_TIME, (Long)(rt.get(REQUEST_TIME)) - (Long)(rt.get(PREPARE_TIME)));
}
}
T tRsp;
try {
// 解析返回结果
String responseBody = (String) rt.get("rsp");
tRsp = parser.parse(responseBody);
tRsp.setBody(responseBody);
if (costTimeMap.containsKey(REQUEST_COST_TIME)) {
costTimeMap.put(POST_COST_TIME, System.currentTimeMillis() - (Long)(rt.get(REQUEST_TIME)));
}
} catch (PlatformApiException e) {
PlatformLogger.logBizError((String) rt.get("rsp"), costTimeMap);
throw new PlatformApiException(e);
}
tRsp.setParams((PlatformHashMap) rt.get("textParams"));
if (!tRsp.isSuccess()) {
PlatformLogger.logErrorScene(rt, tRsp, "", costTimeMap);
} else {
PlatformLogger.logBizSummary(rt, tRsp, costTimeMap);
}
return tRsp;
}
/**
* 发送Post请求
* @param request
* @param
* @return
* @throws PlatformApiException
*/
private Map doPost(PlatformRequest request) throws PlatformApiException {
Map result = new HashMap<>();
RequestParametersHolder requestHolder = getRequestHolder(request);
String url = getRequestUrl(request, requestHolder);
result.put(PREPARE_TIME, System.currentTimeMillis());
String rsp;
try {
rsp = WebUtils.doPost(url, requestHolder.getApplicationParams(), requestHolder.getPropertyParams(), charset,
connectTimeout, readTimeout, null, 0);
} catch (IOException e) {
throw new PlatformApiException(e);
}
result.put(REQUEST_TIME, System.currentTimeMillis());
result.put("rsp", rsp);
result.put("url", url);
return result;
}
/**
* 获取POST请求的base url
*
* @param request
* @return
* @throws PlatformApiException
*/
private String getRequestUrl(PlatformRequest request, RequestParametersHolder requestHolder) throws PlatformApiException {
StringBuilder urlSb = new StringBuilder(serverUrl + request.getApiMappingName());
try {
String sysMustQuery = WebUtils.buildQuery(requestHolder.getProtocalMustParams(),
charset);
urlSb.append("?");
urlSb.append(sysMustQuery);
} catch (IOException e) {
throw new PlatformApiException(e);
}
return urlSb.toString();
}
/**
* 组装接口参数,处理加密、签名逻辑
*
* @param request
* @return
* @throws PlatformApiException
*/
private RequestParametersHolder getRequestHolder(PlatformRequest> request) {
RequestParametersHolder requestHolder = new RequestParametersHolder();
PlatformHashMap appParams = new PlatformHashMap(request.getTextParams());
requestHolder.setApplicationParams(appParams);
// 设置必填参数
if (StringUtils.isEmpty(charset)) {
charset = PlatformConstants.CHARSET_UTF8;
}
PlatformHashMap protocalMustParams = new PlatformHashMap();
protocalMustParams.put(PlatformConstants.CHARSET, charset);
requestHolder.setProtocalMustParams(protocalMustParams);
// 设置请求头参数
PlatformHashMap propertyParams = new PlatformHashMap();
String accessToken = appParams.get(PlatformConstants.ACCESS_TOKEN);
if (StringUtils.isEmpty(accessToken)) {// 当accessToken为空时,需要设置Authorization
String auth = clientId +":" + getClientSecret();
//对其进行加密
byte[] rel = Base64.encodeBase64(auth.getBytes());
String res = new String(rel);
propertyParams.put(PlatformConstants.AUTHORIZATION, "Basic " + res);
}
requestHolder.setPropertyParams(propertyParams);
return requestHolder;
}
public abstract String getClientSecret();
}
DefaultPlatformClient.java
package com.leven.platform.api;
/**
* 默认执行器
* @author Leven
* @date 2019-06-05
*/
public class DefaultPlatformClient extends AbstractPlatformClient {
private String clientSecret;
public DefaultPlatformClient(String serverUrl, String clientId, String clientSecret) {
super(serverUrl, clientId);
this.clientSecret = clientSecret;
}
@Override
public String getClientSecret() {
return clientSecret;
}
}
PlatformParser.java
package com.leven.platform.api;
/**
* 响应解释器接口
* @author Leven
* @date 2019-06-05
*/
public interface PlatformParser {
/**
* 把响应字符串解释成相应的领域对象。
*
* @param rsp 响应字符串
* @return 领域对象
*/
T parse(String rsp) throws PlatformApiException;
/**
* 获取响应类类型。
*/
Class getResponseClass() throws PlatformApiException;
}
PlatformApiException.java
package com.leven.platform.api;
/**
* 平台接口异常
* @author Leven
* @date 2019-06-05
*/
public class PlatformApiException extends RuntimeException {
private static final long serialVersionUID = -238091758285157331L;
private String errCode;
private String errMsg;
public PlatformApiException() {
}
public PlatformApiException(String message, Throwable cause) {
super(message, cause);
this.errCode = "1001";
this.errMsg = message;
}
public PlatformApiException(String message) {
super(message);
this.errCode = "1001";
this.errMsg = message;
}
public PlatformApiException(Throwable cause) {
super(cause);
}
public PlatformApiException(String errCode, String errMsg) {
super(errCode + ":" + errMsg);
this.errCode = errCode;
this.errMsg = errMsg;
}
public String getErrCode() {
return this.errCode;
}
public String getErrMsg() {
return this.errMsg;
}
}
3.4 常量类
PlatformConstants.java
package com.leven.platform.api;
public class PlatformConstants {
private PlatformConstants() {}
/**
* 应用ID
*/
public static final String CLIENT_ID = "client_id";
/**
* 请求路由
*/
public static final String MAPPING = "mapping";
/**
* 请求资源必须带上access_token
*/
public static final String ACCESS_TOKEN = "access_token";
/**
* 授权信息
*/
public static final String AUTHORIZATION = "Authorization";
/**
* 字符集
*/
public static final String CHARSET = "charset";
/**
* 默认时间格式
**/
public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
/**
* Date默认时区
**/
public static final String DATE_TIMEZONE = "GMT+8";
/**
* UTF-8字符集
**/
public static final String CHARSET_UTF8 = "UTF-8";
/**
* JSON 格式
*/
public static final String FORMAT_JSON = "json";
/**
* SDK版本号
*/
public static final String SDK_VERSION = "platform-sdk-1.0.0";
/**
* 请求IP
*/
public static final String REQUEST_IP = "REQUEST_IP";
/**
* 消息类型
*/
public static class MsgType {
private MsgType() {}
/** 网关验证*/
public static final String CHECK_GATEWAY = "CHECK_GATEWAY";
}
}
PlatformEcode.java
package com.leven.platform.api;
import java.io.Serializable;
/**
* 统一接口响应编码
* @author Leven
* @date 2019-06-05
*/
public class PlatformEcode implements Serializable {
public static final String SUCCESS = "1000";
}
3.5 接口实现
这里只例举俩个接口,可以根据业务需要添加其他的。
3.5.1 获取Token接口
请求类 PlatformOAuthTokenRequest.java
package com.leven.platform.api.request;
import com.leven.platform.api.PlatformRequest;
import com.leven.platform.api.internal.util.PlatformHashMap;
import com.leven.platform.api.response.PlatformOAuthTokenResponse;
import lombok.Getter;
import lombok.Setter;
import java.util.Map;
@Getter
@Setter
public class PlatformOAuthTokenRequest implements PlatformRequest {
private String grantType;
private String redirectUri;
private String code;
private String refreshToken;
public PlatformOAuthTokenRequest() {}
@Override
public String getApiMappingName() {
return "/oauth/token";
}
@Override
public Map getTextParams() {
PlatformHashMap txtParams = new PlatformHashMap();
txtParams.put("grant_type", this.grantType);
txtParams.put("redirect_uri", this.redirectUri);
txtParams.put("code", this.code);
txtParams.put("refresh_token", this.refreshToken);
return txtParams;
}
@Override
public Class getResponseClass() {
return PlatformOAuthTokenResponse.class;
}
}
响应类 PlatformOAuthTokenResponse.java
package com.leven.platform.api.response;
import com.leven.platform.api.AbstractPlatformResponse;
import com.leven.platform.api.internal.mapping.ApiField;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ToString
@Getter
@Setter
public class PlatformOAuthTokenResponse extends AbstractPlatformResponse {
@ApiField("access_token")
private String accessToken;
@ApiField("token_type")
private String tokenType;
@ApiField("refresh_token")
private String refreshToken;
@ApiField("expires_in")
private String expiresIn;
@ApiField("scope")
private String scope;
@ApiField("openid")
private String openid;
public PlatformOAuthTokenResponse() {}
}
3.5.2 获取用户信息接口
请求类 PlatformOAuthUserinfoRequest.java
package com.leven.platform.api.request;
import com.leven.platform.api.PlatformRequest;
import com.leven.platform.api.internal.util.PlatformHashMap;
import com.leven.platform.api.response.PlatformOAuthUserinfoResponse;
import lombok.Getter;
import lombok.Setter;
import java.util.Map;
@Getter
@Setter
public class PlatformOAuthUserinfoRequest implements PlatformRequest {
private String accessToken;
private String openid;
@Override
public String getApiMappingName() {
return "/oauth/userinfo";
}
@Override
public Map getTextParams() {
PlatformHashMap txtParams = new PlatformHashMap();
txtParams.put("access_token", this.accessToken);
txtParams.put("openid", this.openid);
return txtParams;
}
@Override
public Class getResponseClass() {
return PlatformOAuthUserinfoResponse.class;
}
}
响应类 PlatformOAuthUserinfoResponse.java
package com.leven.platform.api.response;
import com.leven.platform.api.AbstractPlatformResponse;
import com.leven.platform.api.internal.mapping.ApiField;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ToString
@Getter
@Setter
public class PlatformOAuthUserinfoResponse extends AbstractPlatformResponse {
@ApiField("openid")
private String openid;
@ApiField("username")
private String username;
@ApiField("trueName")
private String trueName;
@ApiField("phone")
private String phone;
@ApiField("idNumber")
private String idNumber;
public PlatformOAuthUserinfoResponse() {}
}
4、消息推送部分
这里我写死了使用JSON格式,其实还可以设计得更灵活。
4.1 消息接收
PlatformJSONMsg.java
package com.leven.platform.api.message;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.io.Serializable;
/**
* JSON消息接收
* @author Leven
* @date 2019-06-12
*/
@Getter
@Setter
public class PlatformJSONMsg implements Serializable {
private String type;
private Long timestamp;
private String content;
private String sign;
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}
4.2 消息回复
PlatformJSONOutMsg.java
package com.leven.platform.api.message;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.io.Serializable;
/**
* JSON消息回复
* @author Leven
* @date 2019-06-12
*/
@Getter
@Setter
public class PlatformJSONOutMsg implements Serializable {
private String ecode = "1000";
private String msg = "成功";
private Object data;
private PlatformJSONOutMsg() {}
private PlatformJSONOutMsg(Object data) {
this.data = data;
}
private PlatformJSONOutMsg(String msg) {
this.ecode = "1001";
this.msg = msg;
}
public static PlatformJSONOutMsg success() {
return new PlatformJSONOutMsg();
}
public static PlatformJSONOutMsg success(Object data) {
return new PlatformJSONOutMsg(data);
}
public static PlatformJSONOutMsg fail(String errMsg) {
return new PlatformJSONOutMsg(errMsg);
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}
4.3 消息处理器
统一接口类 PlatformMsgHandler.java
package com.leven.platform.api.message.handler;
import com.leven.platform.api.PlatformApiException;
import com.leven.platform.api.message.PlatformJSONMsg;
import com.leven.platform.api.message.PlatformJSONOutMsg;
import com.leven.platform.api.service.PlatformService;
/**
* 消息处理器接口
* @author Leven
* @date 2019-07-09
*/
public interface PlatformMsgHandler {
/**
* 消息处理
* @param msg json消息
* @param platformService 平台服务接口
* @return 消息回复
* @throws PlatformApiException
*/
PlatformJSONOutMsg handle(PlatformJSONMsg msg, PlatformService platformService) throws PlatformApiException;
}
网关验证处理 CheckGatewayHandler.java
package com.leven.platform.api.message.handler;
import com.leven.platform.api.PlatformApiException;
import com.leven.platform.api.message.PlatformJSONMsg;
import com.leven.platform.api.message.PlatformJSONOutMsg;
import com.leven.platform.api.message.model.CheckGatewayDTO;
import com.leven.platform.api.service.PlatformService;
/**
* 网关验证消息处理器
* @author Leven
* @date 2019-07-09
*/
public class CheckGatewayHandler implements PlatformMsgHandler {
@Override
public PlatformJSONOutMsg handle(PlatformJSONMsg msg, PlatformService platformService) throws PlatformApiException {
CheckGatewayDTO dto = platformService.parseMsg(msg, CheckGatewayDTO.class);
return PlatformJSONOutMsg.success(dto.getEchoStr());
}
}
4.4 消息内容对象
目前只有一种消息,可以根据业务需要添加。
4.4.1 网关验证消息
CheckGatewayDTO.java
package com.leven.platform.api.message.model;
import lombok.Getter;
import lombok.Setter;
/**
* 网关验证
* @author Leven
* @date 2019-07-09
*/
@Getter
@Setter
public class CheckGatewayDTO extends BaseDTO {
/**
* 随机字符串
*/
private String echoStr;
}
5、服务接口部分
5.1 授权服务
PlatformOAuth2Service.java
package com.leven.platform.api.service;
import com.leven.platform.api.PlatformApiException;
import com.leven.platform.api.response.PlatformOAuthTokenResponse;
import com.leven.platform.api.response.PlatformOAuthUserinfoResponse;
/**
* 开放授权服务接口
* @author Leven
* @date 2019-07-09
*/
public interface PlatformOAuth2Service {
/**
* 获取授权链接
* @param serverUrl 平台地址
* @param clientId 应用ID
* @param redirectUri 应用授权回调
* @return 授权链接
* @throws PlatformApiException
*/
String getAuthorizeUri(String serverUrl, String clientId, String redirectUri) throws PlatformApiException;
/**
* 通过code换取access_token
* @param code 授权码
* @return token对象
* @throws PlatformApiException
*/
PlatformOAuthTokenResponse getAccessToken(String code) throws PlatformApiException;
/**
* 刷新access_token
* @param refreshToken 刷新token
* @return token对象
* @throws PlatformApiException
*/
PlatformOAuthTokenResponse refreshAccessToken(String refreshToken) throws PlatformApiException;
/**
* 获取用户信息
* @param accessToken 授权token
* @param openid 用户ID
* @return 用户信息
* @throws PlatformApiException
*/
PlatformOAuthUserinfoResponse getUserinfo(String accessToken, String openid) throws PlatformApiException;
}
PlatformOAuth2ServiceImpl.java
package com.leven.platform.api.service.impl;
import com.leven.platform.api.DefaultPlatformClient;
import com.leven.platform.api.PlatformApiException;
import com.leven.platform.api.PlatformClient;
import com.leven.platform.api.PlatformConstants;
import com.leven.platform.api.config.PlatformConfigStorage;
import com.leven.platform.api.request.PlatformOAuthTokenRequest;
import com.leven.platform.api.request.PlatformOAuthUserinfoRequest;
import com.leven.platform.api.response.PlatformOAuthTokenResponse;
import com.leven.platform.api.response.PlatformOAuthUserinfoResponse;
import com.leven.platform.api.service.PlatformOAuth2Service;
import com.leven.platform.api.service.PlatformService;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
/**
* 开放授权服务接口
* @author Leven
* @date 2019-07-09
*/
public class PlatformOAuth2ServiceImpl implements PlatformOAuth2Service {
/** 授权url*/
private static final String AUTHORIZE_URI = "SERVER_URL/oauth/authorize?client_id=CLIENT_ID&response_type=code" +
"&scope=read&redirect_uri=REDIRECT_URI";
private PlatformService platformService;
private PlatformClient platformClient;
public PlatformClient getPlatformClient() {
PlatformConfigStorage configStorage = platformService.getConfigStorage();
if (platformClient == null) {
platformClient = new DefaultPlatformClient(configStorage.getServerUrl(),
configStorage.getClientId(),
configStorage.getClientSecret());
}
return platformClient;
}
public PlatformOAuth2ServiceImpl(PlatformService platformService) {
this.platformService = platformService;
}
/**
* 获取授权链接
* @param serverUrl 平台地址
* @param clientId 应用ID
* @param redirectUri 应用授权回调
* @return 授权链接
* @throws PlatformApiException
*/
@Override
public String getAuthorizeUri(String serverUrl, String clientId, String redirectUri) throws PlatformApiException {
try {
return AUTHORIZE_URI.replace("SERVER_URL", serverUrl)
.replace("CLIENT_ID", clientId)
.replace("REDIRECT_URI", URLEncoder.encode(redirectUri, PlatformConstants.CHARSET_UTF8));
} catch (UnsupportedEncodingException e) {
throw new PlatformApiException("拼接通行证网页授权url出现异常:", e);
}
}
@Override
public PlatformOAuthTokenResponse getAccessToken(String code) throws PlatformApiException {
String oauth2RedirectUri = platformService.getConfigStorage().getOauth2RedirectUri();
PlatformOAuthTokenRequest request = new PlatformOAuthTokenRequest();
request.setGrantType("authorization_code");
request.setRedirectUri(oauth2RedirectUri);
request.setCode(code);
PlatformOAuthTokenResponse response = getPlatformClient().execute(request);
if (!response.isSuccess()) {
throw new PlatformApiException(String.format("调用平台接口获取access_token失败:%s", response.getMsg()));
}
return response;
}
/**
* 刷新access_token
* @param refreshToken 刷新token
* @return token对象
* @throws PlatformApiException
*/
@Override
public PlatformOAuthTokenResponse refreshAccessToken(String refreshToken) throws PlatformApiException {
PlatformOAuthTokenRequest request = new PlatformOAuthTokenRequest();
request.setGrantType("refresh_token");
request.setRefreshToken(refreshToken);
PlatformOAuthTokenResponse response = getPlatformClient().execute(request);
if (!response.isSuccess()) {
throw new PlatformApiException(String.format("调用平台接口刷新access_token失败:%s", response.getMsg()));
}
return response;
}
/**
* 获取通行证信息
* @param accessToken 授权token
* @param openid 通行证ID
* @return 通行证信息
* @throws PlatformApiException
*/
@Override
public PlatformOAuthUserinfoResponse getUserinfo(String accessToken, String openid) throws PlatformApiException {
PlatformOAuthUserinfoRequest request = new PlatformOAuthUserinfoRequest();
request.setAccessToken(accessToken);
request.setOpenid(openid);
PlatformOAuthUserinfoResponse response = getPlatformClient().execute(request);
if (!response.isSuccess()) {
throw new PlatformApiException(String.format("调用平台接口获取通行证信息失败:%s", response.getMsg()));
}
return response;
}
}
5.2 平台服务
PlatformService.java
package com.leven.platform.api.service;
import com.leven.platform.api.PlatformApiException;
import com.leven.platform.api.config.PlatformConfigStorage;
import com.leven.platform.api.message.PlatformJSONMsg;
import javax.servlet.http.HttpServletRequest;
/**
* 平台服务接口
* @author Leven
* @date 2019-07-09
*/
public interface PlatformService {
/**
* 获取配置存储
* @return
*/
PlatformConfigStorage getConfigStorage();
/**
* 设置配置存储
* @param configProvider
*/
void setConfigStorage(PlatformConfigStorage configProvider);
/**
* 消息验证
* @param msg
* @throws PlatformApiException
*/
void checkMsg(PlatformJSONMsg msg) throws PlatformApiException;
/**
* 解析消息
* @param msg
* @param clazz
* @param
* @return
* @throws PlatformApiException
*/
T parseMsg(PlatformJSONMsg msg, Class clazz) throws PlatformApiException;
/**
* 获取开放授权服务接口
* @return
*/
PlatformOAuth2Service getOAuth2Service();
/**
* 从request中读取json消息
* @param req
* @return
* @throws PlatformApiException
*/
PlatformJSONMsg getJSONMsg(HttpServletRequest req) throws PlatformApiException;
}
AbstractPlatformServiceImpl.java
package com.leven.platform.api.service.impl;
import com.leven.platform.api.PlatformApiException;
import com.leven.platform.api.config.PlatformConfigStorage;
import com.leven.platform.api.internal.util.PlatformUtils;
import com.leven.platform.api.internal.util.SHA1SignUtils;
import com.leven.platform.api.internal.util.StringUtils;
import com.leven.platform.api.message.PlatformJSONMsg;
import com.leven.platform.api.service.PlatformOAuth2Service;
import com.leven.platform.api.service.PlatformService;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* 平台服务接口抽象实现
* @author Leven
* @date 2019-07-09
*/
public class AbstractPlatformServiceImpl implements PlatformService {
protected PlatformConfigStorage configStorage;
private PlatformOAuth2Service oAuth2Service = new PlatformOAuth2ServiceImpl(this);
public AbstractPlatformServiceImpl() {}
/**
* 获取配置存储
* @return
*/
@Override
public PlatformConfigStorage getConfigStorage() {
return this.configStorage;
}
/**
* 设置配置存储
* @param configProvider
*/
@Override
public void setConfigStorage(PlatformConfigStorage configProvider) {
this.configStorage = configProvider;
}
/**
* 消息验证
* @param msg 平台json消息
* @throws PlatformApiException
*/
@Override
public void checkMsg(PlatformJSONMsg msg) throws PlatformApiException {
if (msg == null) {
throw new PlatformApiException("平台消息对象为空!");
}
String type = msg.getType();
Long timestamp = msg.getTimestamp();
String content = msg.getContent();
String sign = msg.getSign();
if (StringUtils.isEmpty(type)) {
throw new PlatformApiException("消息类型为空!");
}
if (timestamp == null) {
throw new PlatformApiException("时间戳为空!");
}
if (StringUtils.isEmpty(content)) {
throw new PlatformApiException("消息内容为空!");
}
if (StringUtils.isEmpty(sign)) {
throw new PlatformApiException("签名为空!");
}
checkSignature(msg, configStorage.getEncodingAESKey());
}
/**
* 解析消息
* @param msg 平台json消息
* @param clazz 消息类
* @param 消息对象
* @return 消息对象
* @throws PlatformApiException
*/
@Override
public T parseMsg(PlatformJSONMsg msg, Class clazz) throws PlatformApiException {
return PlatformUtils.parseMsg(msg, configStorage.getEncodingAESKey(), clazz);
}
/**
* 获取开放授权服务接口
* @return
*/
@Override
public PlatformOAuth2Service getOAuth2Service() {
return this.oAuth2Service;
}
/**
* 从request中读取json消息
* @param req
* @return
* @throws PlatformApiException
*/
@SuppressWarnings("unchecked")
@Override
public PlatformJSONMsg getJSONMsg(HttpServletRequest req) throws PlatformApiException {
try {
BufferedReader br = null;
br = new BufferedReader(new InputStreamReader(req.getInputStream(), StandardCharsets.UTF_8));
String line;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null) {
sb.append(line);
}
Map map = (Map) PlatformUtils.parseJson(sb.toString());
return PlatformUtils.mapToBean(map, PlatformJSONMsg.class);
} catch (Exception e) {
throw new PlatformApiException("解析平台发送的消息出现异常:", e);
}
}
/**
* 验证签名
* @param msg 平台json消息
* @param encodingAESKey 消息加密密钥
* @throws PlatformApiException
*/
private void checkSignature(PlatformJSONMsg msg, String encodingAESKey) throws PlatformApiException {
Map paramMap = new HashMap<>(4);
paramMap.put("type", msg.getType());
paramMap.put("timestamp", String.valueOf(msg.getTimestamp()));
paramMap.put("content", msg.getContent());
String sign = SHA1SignUtils.sign(encodingAESKey, paramMap);
if (!sign.equalsIgnoreCase(msg.getSign())) {
throw new PlatformApiException("签名错误!");
}
}
}
DefaultPlatformServiceImpl.java
package com.leven.platform.api.service.impl;
/**
* 平台服务接口默认实现
* @author Leven
* @date 2019-07-09
*/
public class DefaultPlatformServiceImpl extends AbstractPlatformServiceImpl {
}
四、使用SDK
这里以SpringBoot项目为例,当然其他框架也是可以的。
1、引入jar
com.leven
platform-sdk
${platform.version}
2、添加配置
application.yml
# 平台配置
platform:
# 服务地址
server-url: http://192.168.199.2:8008/platform
# 应用ID
client-id: p9r4i6zbj6jt81e62
# 应用密钥
client-secret: 868gpxep0kpjf19f2hq9xritbd4sno1x
# 消息加密密钥
encoding-AES-key: TfTaPJCAU54YcnucQAYeFm26Htwf2qs0
# 应用授权回调地址
oauth2-redirect-uri: http://192.168.199.2:8088/platform/redirect
# 前端登录地址(前后端分离项目)
front-login-uri: http://192.168.199.2:8088/#/platform/login
PlatformProperties.java
package com.leven.visual.service.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 平台配置
*/
@Component
@ConfigurationProperties(prefix = "platform")
@Data
public class PlatformProperties {
/**
* 服务地址
*/
private String serverUrl;
/**
* 应用ID
*/
private String clientId;
/**
* 应用ID
*/
private String clientSecret;
/**
* 消息加密密钥
*/
private String encodingAESKey;
/**
* 应用授权回调地址
*/
private String oauth2RedirectUri;
/**
* 前端登录地址
*/
private String frontLoginUri;
}
3、注入平台服务
/**
* 注入平台服务接口
* @return
*/
@Bean
public PlatformService platformService() {
PlatformService platformService = new DefaultPlatformServiceImpl();
PlatformConfigStorage configStorage = new MemoryConfigStorage();
configStorage.setServerUrl(platformProperties.getServerUrl());
configStorage.setClientId(platformProperties.getClientId());
configStorage.setClientSecret(platformProperties.getClientSecret());
configStorage.setEncodingAESKey(platformProperties.getEncodingAESKey());
configStorage.setOauth2RedirectUri(platformProperties.getOauth2RedirectUri());
platformService.setConfigStorage(configStorage);
return platformService;
}
4、controller实现
PlatformController.java
package com.leven.visual.web.controller;
import com.leven.commons.model.exception.BasicEcode;
import com.leven.commons.model.exception.SPIException;
import com.leven.platform.api.PlatformApiException;
import com.leven.platform.api.PlatformConstants;
import com.leven.platform.api.message.PlatformJSONMsg;
import com.leven.platform.api.message.PlatformJSONOutMsg;
import com.leven.platform.api.message.handler.CheckGatewayHandler;
import com.leven.platform.api.message.router.PlatformMsgRouter;
import com.leven.platform.api.response.PlatformOAuthTokenResponse;
import com.leven.platform.api.response.PlatformOAuthUserinfoResponse;
import com.leven.platform.api.service.PlatformOAuth2Service;
import com.leven.platform.api.service.PlatformService;
import com.leven.visual.service.properties.PlatformProperties;
import com.leven.visual.service.properties.WebProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* 接入平台
* @author Leven
* @date 2019-07-02
*/
@Slf4j
@Controller
@RequestMapping("platform")
public class PlatformController {
/**
* 平台配置
*/
@Autowired
private PlatformProperties platformProperties;
/**
* web配置
*/
@Autowired
private WebProperties webProperties;
/**
* 平台服务接口
*/
@Autowired
private PlatformService platformService;
/**
* 应用网关
*
* @param jsonMsg 平台json消息
* @return 消息响应结果
*/
@ResponseBody
@PostMapping("gateway")
public PlatformJSONOutMsg gateway(@RequestBody PlatformJSONMsg jsonMsg) {
try {
// 消息验证
platformService.checkMsg(jsonMsg);
// 创建消息路由,开始处理消息
PlatformMsgRouter router = new PlatformMsgRouter(platformService);
// 设置路由匹配规则和消息处理器
router.rule().msgType(PlatformConstants.MsgType.CHECK_GATEWAY)
.handler(new CheckGatewayHandler()).end();
// 消息处理成功,返回结果
return router.route(jsonMsg);
} catch (PlatformApiException e) {
log.error("应用网关处理出错,调用平台API发生异常:", e);
return PlatformJSONOutMsg.fail(e.getErrMsg());
}
}
/**
* 应用授权回调
*
* @param code 授权码
* @param req servlet请求
* @param res servlet响应
*/
@GetMapping("redirect")
public void redirect(String code, HttpServletRequest req, HttpServletResponse res) {
HttpSession session = req.getSession();
// 前端登录地址(前后端分离项目)
String redirect = platformProperties.getFrontLoginUri();
try {
PlatformOAuth2Service oAuth2Service = platformService.getOAuth2Service();
// 通过code获取access_token
PlatformOAuthTokenResponse tokenResponse = oAuth2Service.getAccessToken(code);
// 通过access_token获取通行证信息
PlatformOAuthUserinfoResponse userinfoResponse = oAuth2Service.getUserinfo(tokenResponse.getAccessToken(),
tokenResponse.getOpenid());
// 登录用户名
String username = userinfoResponse.getUsername();
// 设置session
...
} catch (PlatformApiException e) {
log.error("应用授权回调处理出错,调用平台API发生异常:", e);
// 出现异常,跳转到前端错误页面
redirect = webProperties.getErrorUrl();
} catch (SPIException e) {
log.error("应用授权回调处理出错,调用业务接口发生异常:", e);
// 出现异常,跳转到前端错误页面
redirect = webProperties.getErrorUrl();
}
try {
// 执行跳转
res.sendRedirect(redirect);
} catch (IOException e) {
throw new SPIException(BasicEcode.ILLEGAL_PARAMETER);
}
}
}
五、后续
本文为思否原创文章,未经允许不得转载。
如读者发现有错误,欢迎留言!