受权码模式应用是最宽泛,而且大多数互联网站点都应用了此模式,如第三方应用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
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.9.RELEASE</version> <relativePath/></parent>
spring security
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.6.RELEASE</version></dependency>
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<String, PasswordEncoder> 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")@Datapublic 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@Componentpublic 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@Servicepublic 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<GrantedAuthority> 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 */@Slf4jpublic 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@Setterpublic 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@EnableAuthorizationServerpublic 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<String, Object> 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 */@Componentpublic 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 */@Componentpublic 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援用
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId></dependency><dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version></dependency><dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId></dependency><dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId></dependency><dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.73</version></dependency>
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 <T> 响应类 * @author Leven * @date 2019-06-05 */public interface PlatformRequest <T extends AbstractPlatformResponse> { /** * 获取API的映射路由。 * * @return API名称 */ String getApiMappingName(); /** * 获取所有的Key-Value模式的文本申请参数汇合。其中: * <ul> * <li>Key: 申请参数名</li> * <li>Value: 申请参数值</li> * </ul> * * @return 文本申请参数汇合 */ Map<String, String> getTextParams(); /** * 失去以后API的响应后果类型 * * @return 响应类型 */ Class<T> 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@Setterpublic abstract class AbstractPlatformResponse implements Serializable { private String ecode; private String msg; private String body; private Map<String, String> 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 extends AbstractPlatformResponse> T execute(PlatformRequest<T> request) throws PlatformApiException { PlatformParser<T> parser = null; if (PlatformConstants.FORMAT_JSON.equals(this.format)) { parser = new ObjectJsonParser<>(request.getResponseClass()); } return execute(request, parser); } private <T extends AbstractPlatformResponse> T execute(PlatformRequest<T> request, PlatformParser<T> parser) throws PlatformApiException { long beginTime = System.currentTimeMillis(); Map<String, Object> rt = doPost(request); Map<String, Long> 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 <T> * @return * @throws PlatformApiException */ private <T extends AbstractPlatformResponse> Map<String, Object> doPost(PlatformRequest<T> request) throws PlatformApiException { Map<String, Object> 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<T extends AbstractPlatformResponse> { /** * 把响应字符串解释成相应的畛域对象。 * * @param rsp 响应字符串 * @return 畛域对象 */ T parse(String rsp) throws PlatformApiException; /** * 获取响应类类型。 */ Class<T> 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@Setterpublic class PlatformOAuthTokenRequest implements PlatformRequest<PlatformOAuthTokenResponse> { private String grantType; private String redirectUri; private String code; private String refreshToken; public PlatformOAuthTokenRequest() {} @Override public String getApiMappingName() { return "/oauth/token"; } @Override public Map<String, String> 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<PlatformOAuthTokenResponse> 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@Setterpublic 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@Setterpublic class PlatformOAuthUserinfoRequest implements PlatformRequest<PlatformOAuthUserinfoResponse> { private String accessToken; private String openid; @Override public String getApiMappingName() { return "/oauth/userinfo"; } @Override public Map<String, String> getTextParams() { PlatformHashMap txtParams = new PlatformHashMap(); txtParams.put("access_token", this.accessToken); txtParams.put("openid", this.openid); return txtParams; } @Override public Class<PlatformOAuthUserinfoResponse> 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@Setterpublic 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@Setterpublic 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@Setterpublic 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@Setterpublic 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 <T> * @return * @throws PlatformApiException */ <T> T parseMsg(PlatformJSONMsg msg, Class<T> 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 <T> 音讯对象 * @return 音讯对象 * @throws PlatformApiException */ @Override public <T> T parseMsg(PlatformJSONMsg msg, Class<T> 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<String, Object> map = (Map<String, Object>) 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<String, String> 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
<dependency> <groupId>com.leven</groupId> <artifactId>platform-sdk</artifactId> <version>${platform.version}</version></dependency>
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")@Datapublic class PlatformProperties { /** * 服务地址 */ private String serverUrl; /** * 利用ID */ private String clientId; /** * 利用ID */ private String clientSecret; /** * 音讯加密密钥 */ private String encodingAESKey; /** * 利用受权回调地址 */ private String oauth2RedirectUri; /** * 前端登录地址 */ private String frontLoginUri;}
3、注入平台服务
/** * 注入平台服务接口 * @return */@Beanpublic 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); } }}
五、后续
本文为思否原创文章,未经容许不得转载。
如读者发现有谬误,欢送留言!