受权码模式应用是最宽泛,而且大多数互联网站点都应用了此模式,如第三方应用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);        }    }}

五、后续

本文为思否原创文章,未经容许不得转载。
如读者发现有谬误,欢送留言!