使用Spring Security OAuth 开发APP认证框架
in JavaDevelop with 0 comment

使用Spring Security OAuth 开发APP认证框架

in JavaDevelop with 0 comment

SpringSecurityOAuth简介

传统方式:基于session

基于token方式:oauth

SpringSecurityOAuth封装了服务提供商大部分的操作;而social则是封装了客户端和服务提供商交互的流程

协议中没有规定token要怎么生成和存储。spring oath中规定了;
除了4种的标准模式;让我们自己的自定义验证也添加到该流程中,相当于自定义认证?

本章内容简介:

实现标准的OAuth服务提供商

写在app中,所以demo项目的依赖需要修改下

dependencies {
//    compile project(':security-browser') // 开发app,先暂时注释掉
    compile project(':security-app')

本次依赖更改出错的地方有:

cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCodeFilter
中需要两个处理器,在app中先复制一份出来

com.example.demo.security.MyUserDetailsService#passwordEncoder
passwordEncoder 之前写在browser的,抽取到core里面

依赖

// security自动配置
// 以及包含了 spring-cloud-start,spring-cloud-security 、spring-boot-starter-actuator
// security 5+ 去掉了可以在配置文件中关闭security的配置,所以这里在视频中配置关闭的时候
// 我们在这里注释掉依赖就可以了
//    compile('org.springframework.cloud:spring-cloud-starter-security')
// 多包涵了一个spring-security-oauth2-autoconfigure
compile('org.springframework.cloud:spring-cloud-starter-oauth2')

认证服务器

package cn.mrcode.imooc.springsecurity.securityapp;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

@Configuration
@EnableAuthorizationServer  // 添加一个配置类即可
public class MyAuthorizationServerConfig {
}

启动项目会自动增加以下几个控制器

"{[/oauth/authorize]}"
"{[/oauth/authorize],methods=[POST],
"{[/oauth/token],methods=[GET]}"
"{[/oauth/token],methods=[POST]}"
"{[/oauth/check_token]}"
"{[/oauth/confirm_access]}"
"{[/oauth/error]}"

且在控制台会打印一个默认的clientid(每次都动态生成)

security.oauth2.client.client-id = 666b1e3c-bbec-4a6c-86ca-3387dd113519
security.oauth2.client.client-secret = eea6a558-ce58-4d82-b553-b70406005c8b

可以修改成固定的,方便后面的调试

security:
  oauth2:
    client:
      client-id: myid
      client-secret: myid

授权码模式-授权

官网文档 https://tools.ietf.org/html/rfc6749#section-4

由于spring oath2实现的是标准的oat2协议,所以参数什么的一般可以参考官网文档,如上链接。
获得授权部分

需要在浏览器中访问(因为有跳转):访问以下地址缺报错了。

http://localhost:8080/oauth/authorize?response_type=code&client_id=myid&redirect_uri=http://www.example.com&scope=all

There was an unexpected error (type=Internal Server Error, status=500).
User must be authenticated with Spring Security before authorization can be completed.

不知道为什么一直走
org.springframework.security.web.authentication.AnonymousAuthenticationFilter#doFilter

在这个报错的地方很容易找到,里面说必须要经过security的安全认证。

现在终于串联起来了。之前看到过一篇文章在WebSecurityConfigurerAdapter配置类中打开了运行表单认证
然后就可以访问了。原来是这样。视频中的版本是直接默认basic认证的,所以不需要配置什么;
@Override
       public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
           //允许表单认证
           oauthServer.allowFormAuthenticationForClients();
       }

这里收必须要经过security认证才可以,视频中是直接跳出来一个 basic登录框。

** 重要的事情说三遍:security5+ 认证默认为表单了也就是http.formLogin() **

** 重要的事情说三遍:security5+ 认证默认为表单了也就是http.formLogin() **

** 重要的事情说三遍:security5+ 认证默认为表单了也就是http.formLogin() **

所以这里还需要把security的默认表单登录改成basic登录

@Configuration
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic();
    }
}

注意: 只有正确的在 basic 登录框中输入了用户名密码,才会跳转,不然这个弹框会一直无限弹出的。

再次登录再次报错:

error="invalid_request", error_description="At least one redirect_uri must be registered with the client."
``

报错点
```java
org.springframework.security.oauth2.provider.endpoint.DefaultRedirectResolver#resolveRedirect

/**
 * The pre-defined redirect URI for this client to use during the "authorization_code" access grant. See OAuth spec,
 * section 4.1.1.
 *
 * @return The pre-defined redirect URI for this client.
 */
Set<String> getRegisteredRedirectUri();

打开看了下规范,也没有太看明白。应该和qq登录那边一样的,需要设置一个授权回调域

而在代码中报错的地方调试最后发现

org.springframework.boot.autoconfigure.security.oauth2.authserver.OAuth2AuthorizationServerConfiguration.BaseClientDetailsConfiguration#oauth2ClientDetails

@Bean
		@ConfigurationProperties(prefix = "security.oauth2.client")
		public BaseClientDetails oauth2ClientDetails() {
			BaseClientDetails details = new BaseClientDetails();
			if (this.client.getClientId() == null) {
				this.client.setClientId(UUID.randomUUID().toString());
			}
			details.setClientId(this.client.getClientId());
			details.setClientSecret(this.client.getClientSecret());
			details.setAuthorizedGrantTypes(Arrays.asList("authorization_code",
					"password", "client_credentials", "implicit", "refresh_token"));
			details.setAuthorities(
					AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
			details.setRegisteredRedirectUri(Collections.<String>emptySet());
			return details;
		}

也就是说需要给client配置回调域

security:
  oauth2:
    client:
      client-id: myid
      client-secret: myid
      registered-redirect-uri:
        - "http://example.com"
        - "http://ora.com"

再次访问,出现了久违的需要同意授权的页面,授权后,跳转到了

http://example.com/?code=CXf9ot

授权码模式-获取token

获取token的端点在

org.springframework.security.oauth2.provider.endpoint.TokenEndpoint#postAccessToken

只支持post请求,所以使用postman这样的工具发送;

发送的参数: 参考oath2文档:https://tools.ietf.org/html/rfc6749#section-4.1.3

** 唯一需要注意的是:** basic auth 填写用户名和密码的时候不是之前用admin和123登录的用户信息;

basic信息:是client信息

client-id: myid
client-secret: myid

POST /oauth/token HTTP/1.1
Host: localhost:8080
Authorization: Basic bXlpZDpteWlk
Content-Type: application/x-www-form-urlencoded

参数如下:
code=CXf9ot
grant_type=authorization_code
redirect_uri=http://example.com/
client_id=myid
scope=all

响应如下

{
    "access_token": "2836f983-bbe8-41d4-a2e0-adcaf8cb495b",
    "token_type": "bearer",
    "refresh_token": "e6bbbf09-1fab-4676-8b0c-03afc843fb27",
    "expires_in": 43195,
    "scope": "all"
}

注意: 如果报错,那么就把 redirect_uri=http://example.com/ 换成 http://www.example.com

{
    "error": "invalid_grant",
    "error_description": "Redirect URI mismatch."
}

密码授权模式

basic信息:是client信息

POST /oauth/token HTTP/1.1
Host: localhost:8080
Authorization: Basic bXlpZDpteWlk
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache
Postman-Token: 56bd10f5-27fc-297e-047e-78f71bf89d94

grant_type=password&redirect_uri=http%3A%2F%2Fwww.example.com&client_id=myid&scope=all&username=admin&password=123456

视频中说要添加一个 ROLE_USER 的角色;我这里特意UserDetails的角色赋值为null也可以,
但是在调试的时候发现 authorities 被赋值了一个 ROLE_USER

注:授权码模式和密码模式获取的token是同一个,因为他们都用到了相同的client信息和同一个用户名密码

下面的客户端模式,只用到了client信息没有用户信息,所以和前面的不一样

客户端模式

https://tools.ietf.org/html/rfc6749#section-4.4

basic信息:是client信息

POST /oauth/token HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Authorization: Basic bXlpZDpteWlk
Cache-Control: no-cache
Postman-Token: 17c7f491-401c-f80b-5db2-db1f13bce8d7

grant_type=client_credentials&scope=all

资源服务器

注意: 不加资源服务器的时候,貌似任意服务都不能访问。不知道是不是配置了basic认证的问题

后补:根据笔记实践的时候,只加 @EnableAuthorizationServer 配置,是不会拦截任何访问的,
只有当开启了 资源服务器 @EnableResourceServer,访问任意资源才会被拦截,需要带上 token 进行访问

package cn.mrcode.imooc.springsecurity.securitycore;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;

@Configuration
@EnableResourceServer
public class MyResourcesServerConfig {
}

访问任何信息都会报错:Full authentication is required to access this resourceunauthorized
是因为没有携带token。

携带token访问资源

假设获取到token是

{
    "access_token": "fdaf3e93-9da4-4e7c-a319-79d50c96b997",
    "token_type": "bearer",
    "expires_in": 42587,
    "scope": "all"
}

访问资源:可使用get请求参数:
http://localhost:8080/user/me?access_token=99800232-2564-4c72-9aae-f5d8594c4707

或则使用请求头模式:
Authorization后面的 bearer就是对应上面返回的 token_type;后面是token

GET /user/me HTTP/1.1
Host: localhost:8080
Authorization: bearer 99800232-2564-4c72-9aae-f5d8594c4707

重构用户名密码登录

让自己的逻辑获取token的话,oath前面的逻辑都不能使用。使用我们自己的逻辑来代替。
也就是相当于只使用后面的功能;把自定义认证模式添加进来

在AuthenticationSuccessHandle中存在authentication对象,

所以只要获取到 ClientDetails和TokenRequest即可; 有时间了查看源码找这些吧

思路

  1. 提交登录请求
  2. 登录成功之后,需要在上图AuthenticationSuccessHandler中获取相关信息
  1. 然后走后面的逻辑

处理登录后的逻辑

package cn.mrcode.imooc.springsecurity.securitycore.authentication;

import cn.mrcode.imooc.springsecurity.securitycore.properties.LoginType;
import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityProperties;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException;
import org.springframework.security.oauth2.config.annotation.configuration.ClientDetailsServiceConfiguration;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Base64;

/**
 * 复制之前写好的handler进行修改,只支持app这种模式
 * @author zhuqiang
 * @version 1.0.1 2018/8/3 16:29
 * @date 2018/8/3 16:29
 * @since 1.0
 */
@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    private org.slf4j.Logger logger = LoggerFactory.getLogger(getClass());

    //  com.fasterxml.jackson.databind.
    // spring 是使用jackson来进行处理返回数据的
    // 所以这里可以得到他的实例
    @Autowired
    private com.fasterxml.jackson.databind.ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    /**
     * 授权服务器:自动配置的
     * @see ClientDetailsServiceConfiguration#clientDetailsService()
     */
    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthorizationServerTokenServices authorizationServerTokenServices;

    /**
     * @param request
     * @param response
     * @param authentication 封装了所有的认证信息
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        logger.info("登录成功");
        /**
         * @see BasicAuthenticationFilter#doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain)
         *  */
        String header = request.getHeader("Authorization");

        if (header == null || !header.startsWith("Basic ")) {
            // 不被认可的客户端异常
            throw new UnapprovedClientAuthenticationException("没有Authorization请求头");
        }

        // 解析请Authorization 获取client信息
        // client-id: myid
        // client-secret: myid
        String[] tokens = extractAndDecodeHeader(header, request);
        assert tokens.length == 2;
        String clientId = tokens[0];
        String clientSecret = tokens[1];
        ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
        // 判定提交的是否与查询的匹配

        if (clientDetails == null) {
            throw new UnapprovedClientAuthenticationException("clientId对应的配置信息不存在:" + clientId);
        } else if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) {
            throw new UnapprovedClientAuthenticationException("clientSecret不匹配:" + clientId);
        }

        /**  @see DefaultOAuth2RequestFactory#createTokenRequest(java.util.Map, org.springframework.security.oauth2.provider.ClientDetails)
         * requestParameters,不同的授权模式有不同的参数,这里自定义的模式,没有参数
         * String clientId,
         * Collection<String> scope, 给自己的前段使用,默认用所有的即可
         * String grantType 自定义
         *
         * 在这里我就有一个疑问了:这个token应该代表的是不同的用户,这里使用我们配置的同一个client?那么获取到的不就是相同的token?
         * 难道说是根据用户名和密码创建的?以后明白了再来填坑

         * 后补填坑:org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter 跟源码这里
         * 能看到,之前在发送 token 的时候 其实是和 用户信息(针对当前这个场景流程来说)关联上的,并且放入了 tokenService 中
         * 验证的时候从 tokenService 中获取出来的
         * */

        TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_SORTED_MAP, clientId, clientDetails.getScope(), "costom");
        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);

        /**
         * @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#getOAuth2Authentication(org.springframework.security.oauth2.provider.ClientDetails, org.springframework.security.oauth2.provider.TokenRequest)
         * */
        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);

        // 在后面测试的时候居然抛出了一个 事物异常 Could not open JDBC Connection for transaction; nested exception is ja
        // 我的数据库密码写错了,这个方法上加了一个@Transactional注解
        OAuth2AccessToken accessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(accessToken));
    }

    /**
     * Decodes the header into a username and password.
     * @throws BadCredentialsException if the Basic header is not present or is not valid
     *                                 Base64
     */
    private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException {

        byte[] base64Token = header.substring(6).getBytes("UTF-8");
        byte[] decoded;
        try {
            decoded = Base64.getDecoder().decode(base64Token);
        } catch (IllegalArgumentException e) {
            throw new BadCredentialsException(
                    "Failed to decode basic authentication token");
        }

        String token = new String(decoded, "UTF-8");

        int delim = token.indexOf(":");

        if (delim == -1) {
            throw new BadCredentialsException("Invalid basic authentication token");
        }
        return new String[]{token.substring(0, delim), token.substring(delim + 1)};
    }
}

资源服务器安全配置

其实资源服务器的安全配置就类似普通服务的security的配置,对资源的保护;
spring oath2是建立的security的逻辑上的;不做用户认证,只做授权服务器,发送令牌,验证令牌等功能

所以按照之前配置过的copy过来快速修改,跑起来再细化修改

package cn.mrcode.imooc.springsecurity.securityapp;

/*
 *
 * ${desc}
 * @author zhuqiang
 * @version 1.0.1 2018/8/7 13:15
 * @date 2018/8/7 13:15
 * @since 1.0
 * */

import cn.mrcode.imooc.springsecurity.securitycore.authentication.mobile.SmsCodeAuthenticationSecurityConfig;
import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityConstants;
import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityProperties;
import cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCodeSecurityConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.social.security.SpringSocialConfigurer;

@Configuration
@EnableResourceServer
public class MyResourcesServerConfig extends ResourceServerConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;

    // 由下面的  .apply(smsCodeAuthenticationSecurityConfigs)方法添加这个配置
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfigs;

    @Autowired
    private ValidateCodeSecurityConfig validateCodeSecurityConfig;
    /**
     * @see SocialConfig#imoocSocialSecurityConfig()
     */
    @Autowired
    private SpringSocialConfigurer imoocSocialSecurityConfig;

    @Autowired
    private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler myAuthenticationFailureHandler;


    // 有三个configure的方法,这里使用http参数的
    @Override
    public void configure(HttpSecurity http) throws Exception {

        // 之前配置的security的basic的 不能去掉哦。否则授权码模式又不能使用了
        http.formLogin()
                .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)
                .loginProcessingUrl(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM)
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailureHandler)
        ;
        http
                // 视频中说验证码的功能还有一点问题,先不用
//                .apply(validateCodeSecurityConfig)
//                .and()
                .apply(smsCodeAuthenticationSecurityConfigs)
                .and()
                .apply(imoocSocialSecurityConfig)
                .and()
                // 对请求授权配置:注意方法名的含义,能联想到一些
                .authorizeRequests()
                // 放行这个路径
                .antMatchers(
                        SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
                        SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
                        securityProperties.getBrowser().getLoginPage(),
                        SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*", // 图形验证码接口
                        securityProperties.getBrowser().getSignUpUrl(),  // 注册页面
                        "/user/regist",
                        "/error",
                        "/connect/*",
                        "/auth/*",
                        "/signin"
                )
                .permitAll()
                .anyRequest()
                // 对任意请求都必须是已认证才能访问
                .authenticated()
                .and()
                .csrf()
                .disable()
        ;
    }
}

验证流程是否ok

Authorization 头还是写之前的myid的client信息;访问之前蒂尼的表单登录地址

POST /authentication/form HTTP/1.1
Host: localhost:8080
Authorization: Basic bXlpZDpteWlk
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache
Postman-Token: 932d7563-34c1-002c-e2c1-8923757877c0

username=admin&password=123456

成功获取到token信息

{
    "access_token": "ab9bba42-f954-44d4-8e40-e4d6d81bfe60",
    "token_type": "bearer",
    "refresh_token": "e54ad634-127d-456b-9bf1-3b40c9d43017",
    "expires_in": 43199
}

重构短信登录

现有问题:

  1. 浏览器中使用session存储验证码
  2. app中午cookie概念(无session)

解决方案:

  1. app发送和验证 验证码必须携带一个deviceId (设备id)
  2. 浏览器按之前的逻辑走

也就是说,这里只是验证码的存储发生了变化,那么抽出来一个存储接口,浏览器和app做不同的适配即可

package cn.mrcode.imooc.springsecurity.securitycore.validate.code;

import org.springframework.web.context.request.ServletWebRequest;

/**
 * <pre>
 * 验证码存储仓库接口
 * </pre>
 * @author zhuqiang
 * @version 1.0.0
 * @date 2018/8/8 13:58
 * @since 1.0.0
 */
public interface ValidateCodeRepository {
    /**
     * 保存验证码
     * @param request
     * @param code
     * @param validateCodeType
     */
    void save(ServletWebRequest request, ValidateCode code, ValidateCodeType validateCodeType);

    /**
     * 获取验证码
     * @param request
     * @param validateCodeType
     * @return
     */
    ValidateCode get(ServletWebRequest request, ValidateCodeType validateCodeType);

    /**
     * 移除验证码
     * @param request
     * @param validateCodeType
     */
    void remove(ServletWebRequest request, ValidateCodeType validateCodeType);
}

浏览器实现;(因为开始只有session的实现,重构线把session的抽取出来);
把session相关操作全部抽到这里了

package cn.mrcode.imooc.springsecurity.securitybrowser.validate.code.impl;

import cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCode;
import cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCodeRepository;
import cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCodeType;
import cn.mrcode.imooc.springsecurity.securitycore.validate.code.impl.AbstractValidateCodeProcessor;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;

/**
 * ${desc}
 * @author zhuqiang
 * @version 1.0.1 2018/8/8 14:06
 * @date 2018/8/8 14:06
 * @since 1.0
 */
@Component
public class SessionValidateCodeRepository implements ValidateCodeRepository {
    /** 操作session的工具类 */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    /** 验证码放入session的时候前缀 */
    public final static String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE";

    @Override
    public void save(ServletWebRequest request, ValidateCode code, ValidateCodeType validateCodeType) {
        sessionStrategy.setAttribute(request, getSessionKey(validateCodeType), code);
    }

    @Override
    public ValidateCode get(ServletWebRequest request, ValidateCodeType validateCodeType) {
        String sessionKey = getSessionKey(validateCodeType);
        // 拿到创建 create() 存储到session的code验证码对象
        return (ValidateCode) sessionStrategy.getAttribute(request, sessionKey);
    }

    @Override
    public void remove(ServletWebRequest request, ValidateCodeType validateCodeType) {
        sessionStrategy.removeAttribute(request, getSessionKey(validateCodeType));
    }

    /**
     * 构建验证码放入session时的key; 在保存的时候也使用该key
     * {@link AbstractValidateCodeProcessor#save(org.springframework.web.context.request.ServletWebRequest, cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCode)}
     * @param validateCodeType
     * @return
     */
    private String getSessionKey(ValidateCodeType validateCodeType) {
        return SESSION_KEY_PREFIX + validateCodeType.toString().toUpperCase();
    }
}

app实现

package cn.mrcode.imooc.springsecurity.securityapp.validate.code.impl;

import cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCode;
import cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCodeException;
import cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCodeRepository;
import cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCodeType;
import cn.mrcode.imooc.springsecurity.securitycore.validate.code.impl.AbstractValidateCodeProcessor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;

import java.util.concurrent.TimeUnit;

/**
 * ${desc}
 * @author zhuqiang
 * @version 1.0.1 2018/8/8 14:06
 * @date 2018/8/8 14:06
 * @since 1.0
 */
@Component
public class RedisValidateCodeRepository implements ValidateCodeRepository {
    /**
     * @see RedisAutoConfiguration#redisTemplate(org.springframework.data.redis.connection.RedisConnectionFactory)
     */
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;
    /** 验证码放入redis规则模式:CODE_{TYPE}_{DEVICEId} */
    private final static String CODE_KEY_PATTERN = "CODE_%s_%s";

    @Override
    public void save(ServletWebRequest request, ValidateCode code, ValidateCodeType validateCodeType) {
        redisTemplate.opsForValue().set(buildKey(request, validateCodeType), code, 180, TimeUnit.MINUTES);
    }

    @Override
    public ValidateCode get(ServletWebRequest request, ValidateCodeType validateCodeType) {
        String key = buildKey(request, validateCodeType);
        // 拿到创建 create() 存储到session的code验证码对象
        return (ValidateCode) redisTemplate.opsForValue().get(key);
    }

    @Override
    public void remove(ServletWebRequest request, ValidateCodeType validateCodeType) {
        String key = buildKey(request, validateCodeType);
        redisTemplate.delete(key);
    }

    /**
     * 构建验证码放入redis时的key; 在保存的时候也使用该key
     * {@link AbstractValidateCodeProcessor#save(ServletWebRequest, ValidateCode)}
     * @param validateCodeType
     * @return
     */
    private String buildKey(ServletWebRequest request, ValidateCodeType validateCodeType) {
        String deviceId = request.getHeader("deviceId");
        if (StringUtils.isBlank(deviceId)) {
            throw new ValidateCodeException("请在请求头中携带deviceId参数");
        }
        return String.format(CODE_KEY_PATTERN, validateCodeType, deviceId);
    }
}

测试

打开之前屏蔽掉的图形验证码过滤器配置
发送验证码的是不用登陆的

GET /code/sms?mobile=13012345678 HTTP/1.1
Host: localhost:8080
deviceId: 1

登陆,记得携带client信息

POST /authentication/mobile HTTP/1.1
Host: localhost:8080
Authorization: Basic bXlpZDpteWlk
deviceId: 1
Content-Type: application/x-www-form-urlencoded

smsCode=270058&mobile=13012345678

因为图形验证码和短信验证码存储只有存储这一块变更了。所以只要短信验证码的可以使用,图形验证码的也应该可以使用

重构社交登录

app里面的第三方登录不向浏览器中一样,一般是通过调用sdk,引导到第三方app应用登录后返回;

浏览器模式

可能以下两种模式;

简化模式


上图来看,拿到openId之后,只要我们支持使用openid登录,即可;

可以大部分模仿短信验证码登录的代码,只有一点不同,提交的openid是属于social表中的数据,
所以相关的用户信息SocialUserDetailsService和用户连接信息UsersConnectionRepository
需要通过 socaial提供的表来获取校验逻辑

先看配置

package cn.mrcode.imooc.springsecurity.securityapp.social.openid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component;

/**
 * ${desc}
 * @author zhuqiang
 * @version 1.0.1 2018/8/8 15:59
 * @date 2018/8/8 15:59
 * @since 1.0
 */
@Component
public class OpenIdAuthenticationSecurityConfig
        extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private SocialUserDetailsService userDetailsService;

    @Autowired
    private UsersConnectionRepository usersConnectionRepository;

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Override
    public void configure(HttpSecurity builder) throws Exception {
        OpenIdAuthenticationProvider provider = new OpenIdAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setUsersConnectionRepository(usersConnectionRepository);

        OpenIdAuthenticationFilter filter = new OpenIdAuthenticationFilter();
        // 获取manager的是在源码中看到过
        filter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
        filter.setAuthenticationFailureHandler(authenticationFailureHandler);
        filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);

        // 需要一个服务提供商 和 一个过滤器
        builder.
                authenticationProvider(provider)
                .addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
    }
}

服务商

package cn.mrcode.imooc.springsecurity.securityapp.social.openid;

import org.apache.commons.collections.CollectionUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.security.SocialUserDetailsService;

import java.util.HashSet;
import java.util.Set;

/**
 * ${desc}
 * @author zhuqiang
 * @version 1.0.1 2018/8/8 16:14
 * @date 2018/8/8 16:14
 * @since 1.0
 */
public class OpenIdAuthenticationProvider implements AuthenticationProvider {
    // 要使用social的
    private SocialUserDetailsService userDetailsService;

    private UsersConnectionRepository usersConnectionRepository;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 这里和之前短信验证码登录 唯一不同的是
        // 这里是使用社交登录的userDetailsService 和 usersConnectionRepository
        // 因为只有社交信息里面才会存在相关信息
        OpenIdAuthenticationToken authenticationToken = (OpenIdAuthenticationToken) authentication;
        Set<String> providerUserIds = new HashSet<>();
        providerUserIds.add((String) authenticationToken.getPrincipal());
        Set<String> userIds = usersConnectionRepository.findUserIdsConnectedTo(authenticationToken.getProviderId(), providerUserIds);

        if (CollectionUtils.isEmpty(userIds) || userIds.size() != 1) {
            throw new InternalAuthenticationServiceException("无法获取用户信息");
        }

        String userId = userIds.iterator().next();

        UserDetails user = userDetailsService.loadUserByUserId(userId);

        if (user == null) {
            throw new InternalAuthenticationServiceException("无法获取用户信息");
        }

        OpenIdAuthenticationToken authenticationResult = new OpenIdAuthenticationToken(user, user.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return OpenIdAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public SocialUserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(SocialUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    public UsersConnectionRepository getUsersConnectionRepository() {
        return usersConnectionRepository;
    }

    public void setUsersConnectionRepository(UsersConnectionRepository usersConnectionRepository) {
        this.usersConnectionRepository = usersConnectionRepository;
    }
}


过滤器

package cn.mrcode.imooc.springsecurity.securityapp.social.openid;

import cn.mrcode.imooc.springsecurity.securitycore.properties.QQProperties;
import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityConstants;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * ${desc}
 * @author zhuqiang
 * @version 1.0.1 2018/8/8 16:03
 * @date 2018/8/8 16:03
 * @since 1.0
 */
public class OpenIdAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {
    // ~ Static fields/initializers
    // =====================================================================================
    private String openIdParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_OPEN_ID;
    // 服务提供商id,qq还是微信
    /** @see QQProperties#providerId */
    private String providerIdParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_PROVIDERID;
    private boolean postOnly = true;

    // ~ Constructors
    // ===================================================================================================

    public OpenIdAuthenticationFilter() {
        super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_OPEN_ID, "POST"));
    }

    // ~ Methods
    // ========================================================================================================

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String openId = obtainOpenId(request);
        String providerId = obtainProviderId(request);

        if (openId == null) {
            openId = "";
        }
        if (providerId == null) {
            providerId = "";
        }
        openId = openId.trim();

        OpenIdAuthenticationToken authRequest = new OpenIdAuthenticationToken(openId, providerId);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }


    protected String obtainOpenId(HttpServletRequest request) {
        return request.getParameter(openIdParameter);
    }

    private String obtainProviderId(HttpServletRequest request) {
        return request.getParameter(providerIdParameter);
    }

    protected void setDetails(HttpServletRequest request,
                              OpenIdAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    public void setOpenIdParameter(String openIdParameter) {
        Assert.hasText(openIdParameter, "Username parameter must not be empty or null");
        this.openIdParameter = openIdParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getOpenIdParameter() {
        return openIdParameter;
    }
}

token 实体

package cn.mrcode.imooc.springsecurity.securityapp.social.openid;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

import java.util.Collection;

/**
 * ${desc}
 * @author zhuqiang
 * @version 1.0.1 2018/8/8 16:11
 * @date 2018/8/8 16:11
 * @since 1.0
 */
public class OpenIdAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // ~ Instance fields
    // ================================================================================================

    private final Object principal;
    private String providerId;

    // ~ Constructors
    // ===================================================================================================
    public OpenIdAuthenticationToken(Object principal, String providerId) {
        super(null);
        this.principal = principal;
        this.providerId = providerId;
        super.setAuthenticated(true); // must use super, as we override
    }

    public OpenIdAuthenticationToken(Object principal,
                                     Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }

    // ~ Methods
    // ========================================================================================================


    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    public String getProviderId() {
        return providerId;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }


}

资源服务器增加安全配置

cn.mrcode.imooc.springsecurity.securityapp.MyResourcesServerConfig#configure
.and().apply(openIdAuthenticationSecurityConfig)

记得放行openid过滤器的拦截地址                

测试

在数据库中imooc_userconnection找一个之前在网页qq授权登录的openid

POST /authentication/openid HTTP/1.1
Host: localhost:80
Authorization: Basic bXlpZDpteWlk
Content-Type: application/x-www-form-urlencoded

openId=81F03E50B76D6D829F5A4875941567A6&providerId=qq

授权码模式

这个模式。只需要app端把拿到的授权码转发给服务器即可获得授权码;

这里需要注意的是: 这里拿到code。最终返回来的不是qq的accessToken,而是我们自己服务器的accessToken;

上面简化模式是客户端能直接拿到qq的accessToken和openid,

这里授权码模式是客户端只能拿到 code。还需要服务器去走social获取用户信息的步骤,

这里获取到qq的accessToken和openid后。我们拿着客户端传递的我们自己的client信息和这里获取到的openid;

然后走oath2的 token生成逻辑。最后返回

测试思路:

  1. demo引用浏览器环境
  2. 使用浏览器登录后,得到code
  3. 使用工具发送post请求到token地址,带上code和client信息
org.springframework.social.security.provider.OAuth2AuthenticationService#getAuthToken

public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
  String code = request.getParameter("code");
  if (!StringUtils.hasText(code)) {
    OAuth2Parameters params =  new OAuth2Parameters();
    params.setRedirectUri(buildReturnToUrl(request));
    setScope(request, params);
    params.add("state", generateState(connectionFactory, request));
    addCustomParameters(params);
    throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));
  } else if (StringUtils.hasText(code)) {
    try {
      String returnToUrl = buildReturnToUrl(request);
      // 在这里打断点,使用浏览器模块访问qq登录后,会重定向到这里
      // 然后把服务器关闭掉
      // 浏览器中的地址就是带有code的
      AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
      // TODO avoid API call if possible (auth using token would be fine)
      Connection<S> connection = getConnectionFactory().createConnection(accessGrant);
      return new SocialAuthenticationToken(connection, null);
    } catch (RestClientException e) {
      logger.debug("failed to exchange for access", e);
      return null;
    }
  } else {
    return null;
  }
}

我们拿到浏览器中带code的地址,把服务切回app模块,然后在工具中带上client信息访问;

GET /auth/qq?code=ACEB8728F5DE5B32F9C995BEFEB3C065&amp;state=3cb4d5c7-60c6-4e88-b2a3-f34c5a8b176c HTTP/1.1
Host: mrcode.cn
Authorization: Basic bXlpZDpteWlk

我这里成功获取到了token(控制台打印的),但是postman中返回的错误信息

{
    "error": "unauthorized",
    "error_description": "Full authentication is required to access this resource"
}

我跟了源码,发现成功后跳转到了"/";但是为什么不是重定向而是异常? 这个有待跟踪源码了解下

自定义获取第三方用户信息成功后的逻辑处理

之前讲到可以获得过滤器更改自定义注册地址;这里过滤器里面也可以设置一个授权成功的自定义处理器

cn.mrcode.imooc.springsecurity.securitycore.social.MySpringSocialConfigurer

public class MySpringSocialConfigurer extends SpringSocialConfigurer {
    @Override
    protected <T> T postProcess(T object) {
        // org.springframework.security.config.annotation.SecurityConfigurerAdapter.postProcess()
        // 在SocialAuthenticationFilter中配置死的过滤器拦截地址
        // 这样的方法可以更改拦截的前缀
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
        // filter.setFilterProcessesUrl("/oaths");
       filter.setAuthenticationSuccessHandler();  // 可以把处理器添加到这里
        return (T) filter;
    }
}

现在来改造下

定义处理器接口,让使用处来实现,我们使用注解来获取初始化的bean

package cn.mrcode.imooc.springsecurity.securitycore.social;

import org.springframework.social.security.SocialAuthenticationFilter;

/**
 * @author zhailiang
 *
 */
public interface SocialAuthenticationFilterPostProcessor {

	void process(SocialAuthenticationFilter socialAuthenticationFilter);

}

编写配置文件,获取过滤器设置处理器


package cn.mrcode.imooc.springsecurity.securitycore.social;

import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;

/**
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 12:12
 */
public class MySpringSocialConfigurer extends SpringSocialConfigurer {
    private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;

    @Override
    protected <T> T postProcess(T object) {
        // org.springframework.security.config.annotation.SecurityConfigurerAdapter.postProcess()
        // 在SocialAuthenticationFilter中配置死的过滤器拦截地址
        // 这样的方法可以更改拦截的前缀
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
//        filter.setFilterProcessesUrl("/oaths");
//        filter.setAuthenticationSuccessHandler();
        // 让使用处自己获取token成功的逻辑
        if (socialAuthenticationFilterPostProcessor != null) {
            // 在配置初始化的时候,把过滤器传递给使用方,让使用方把处理器注入
            socialAuthenticationFilterPostProcessor.process(filter);
        }
        return (T) filter;
    }

    public SocialAuthenticationFilterPostProcessor getSocialAuthenticationFilterPostProcessor() {
        return socialAuthenticationFilterPostProcessor;
    }

    public void setSocialAuthenticationFilterPostProcessor(SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor) {
        this.socialAuthenticationFilterPostProcessor = socialAuthenticationFilterPostProcessor;
    }
}

配置文件引用

cn.mrcode.imooc.springsecurity.securitycore.social.SpringSocialConfig

@Autowired(required = false)
private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;

@Bean
public SpringSocialConfigurer imoocSocialSecurityConfig() {
    // 默认配置类,进行组件的组装
    // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
    MySpringSocialConfigurer springSocialConfigurer = new MySpringSocialConfigurer();
    springSocialConfigurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
    // 通过注解获取到使用方注入的bean,给我们刚才写的配置类
    springSocialConfigurer.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
    return springSocialConfigurer;
}

app实现该配置,配置处理成功的处理器

/**
 *
 */
package cn.mrcode.imooc.springsecurity.securityapp.social.impl;

import cn.mrcode.imooc.springsecurity.securitycore.social.SocialAuthenticationFilterPostProcessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.stereotype.Component;

/**
 * @author zhailiang
 */
@Component
public class AppSocialAuthenticationFilterPostProcessor implements SocialAuthenticationFilterPostProcessor {

    @Autowired
    private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;

    /**
     * @see cn.mrcode.imooc.springsecurity.securitycore.social.SocialAuthenticationFilterPostProcessor.process
     */
    @Override
    public void process(SocialAuthenticationFilter socialAuthenticationFilter) {
        // 这里设置的其实就是之前  重构用户名密码登录里面实现的 MyAuthenticationSuccessHandler
        socialAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
    }

}

最后再次测试,得到了我们自己系统的 accessToken

重构注册逻辑

在浏览器中的第三方登录回顾:

  1. social 在拿到用户信息之后
  2. 查询数据库没有绑定的用户会跳转到默认的/signUp路径
  3. 提供了一个我们自己的注册页面,拿到用户提交的注册信息,调用social数据库服务,把关联信息写入数据库中。完成注册
  4. 再次登录,数据库中有用户信息,则登录成功

问题:

  1. 上面这个流程问题所在就是 第三方的信息存放在了 session 中;
  2. 还有一个问题,就是第2步会302.需要客户端信息判定并跳转到登录页

所以现在开始改造,改造方案:

  1. 流程完成后,更改跳转的页面到app指定页面,
  2. 根据设备id,我们把信息存放在redis中
  3. 用户注册完成后,提交,再把第三方信息拿出来,合并完成注册

改造

注意: 在改造测试之前把默认注册用户的功能关闭掉
也就是 com.example.demo.security.DemoConnectionSignUp 类

之前的注册地址是在

cn.mrcode.imooc.springsecurity.securitycore.social.SpringSocialConfig#imoocSocialSecurityConfig

@Bean
public SpringSocialConfigurer imoocSocialSecurityConfig() {
    // 默认配置类,进行组件的组装
    // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
    MySpringSocialConfigurer springSocialConfigurer = new MySpringSocialConfigurer();
    springSocialConfigurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
    springSocialConfigurer.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
    return springSocialConfigurer;
}

中设置的,那么先把这个地址更改掉,由于这里在浏览器环境下工作得很好,不要直接修改这里。使用一个技巧替换掉

package cn.mrcode.imooc.springsecurity.securityapp;

import cn.mrcode.imooc.springsecurity.securitycore.social.SpringSocialConfig;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.social.security.SpringSocialConfigurer;
import org.springframework.stereotype.Component;

/**
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/8 23:49
 */
@Component
public class SpringSocialConfigurerPostProcessor implements BeanPostProcessor {
    // 任何bean初始化回调之前
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    //任何bean初始化回调之后
    // 在这里把之前浏览器中配置的注册地址更改为app中的处理控制器
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        /**
         * @see SpringSocialConfig#imoocSocialSecurityConfig()
         */
        if (beanName.equals("imoocSocialSecurityConfig")) {
            SpringSocialConfigurer config = (SpringSocialConfigurer) bean;
            config.signupUrl("/social/signUp");
            return bean;
        }
        return bean;
    }
}

编写处理跳转接收的控制器;用户把信息传递给前段,引用用户注册;

这里的流程还是之前的拿到code,带着client获得我们系统的accessToken信息

由于数据库中没有该openid的用户信息,所以是未授权状态。

这里先简单写下,然后测试看是否能跳转到这里来。是否能从session中获取到第三方信息;

package cn.mrcode.imooc.springsecurity.securityapp;

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

import javax.servlet.http.HttpServletRequest;

/**
 * 处理登录的控制器
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/8 23:56
 */
@RestController
public class AppSecurityController {
    private Logger logger = LoggerFactory.getLogger(getClass());
    @Autowired
    private ProviderSignInUtils providerSignInUtils;

    @GetMapping(value = "/social/signUp")
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public Connection signUp(HttpServletRequest request) {
        Connection<?> connectionFromSession = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
        logger.info(ReflectionToStringBuilder.toString(connectionFromSession, ToStringStyle.JSON_STYLE));
        return connectionFromSession;
    }
}

我使用postman,跟踪源码的确是走了Redirect;但是postman里面没有302状态,
直接走到上面的控制器里面去了。。搞不明白啊;
一直有一个疑惑,不是说app没有session吗?302的话相当于ajax响应。再次发起请求不是同一个session了,怎么拿到信息的呢?

回答:
postman中settings中有一个选项 Automatically follow redirects;关闭掉也就是变成OFF,就不会自动跳转了

关于 /social/signUp 能获取到session信息,也就是302能获取到session:
是因为postman中有服务器带回来的cookie,禁止掉cookie,就会发现获取不到了

// connection unknown, register new user?
    if (signupUrl != null) {
      // store ConnectionData in session and redirect to register page
      sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
      throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
    }

流程测试通了。来把第三方信息存储在redis中,完成解析来的功能

改造第三方信息存储redis中

utils中的写法 参考 ProviderSignInUtils

package cn.mrcode.imooc.springsecurity.securityapp.social;

import cn.mrcode.imooc.springsecurity.securityapp.AppConstants;
import cn.mrcode.imooc.springsecurity.securityapp.AppSecretException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;

/**
 * @author zhuqiang
 * @version 1.0.1 2018/8/9 14:28
 * @date 2018/8/9 14:28
 * @see ProviderSignInUtils 模拟其中部分的功能
 * @since 1.0
 */
@Component
public class AppSignUpUtils {
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;
    // 目前为止都是自动配置的,直接获取即可
    @Autowired
    private UsersConnectionRepository usersConnectionRepository;
    @Autowired
    private ConnectionFactoryLocator connectionFactoryLocator;

    public void saveConnection(ServletWebRequest request, ConnectionData connectionData) {
        redisTemplate.opsForValue().set(buildKey(request), connectionData);
    }

    /**
     * @param userId
     * @param request
     * @see ProviderSignInAttempt#addConnection(java.lang.String, org.springframework.social.connect.ConnectionFactoryLocator, org.springframework.social.connect.UsersConnectionRepository)
     */
    public void doPostSignUp(String userId, ServletWebRequest request) {
        String key = buildKey(request);
        ConnectionData connectionData = (ConnectionData) redisTemplate.opsForValue().get(key);
        usersConnectionRepository.createConnectionRepository(userId).addConnection(getConnection(connectionFactoryLocator, connectionData));
    }

    public Connection<?> getConnection(ConnectionFactoryLocator connectionFactoryLocator, ConnectionData connectionData) {
        return connectionFactoryLocator.getConnectionFactory(connectionData.getProviderId()).createConnection(connectionData);
    }

    private String buildKey(ServletWebRequest request) {
        String deviceId = request.getHeader(AppConstants.DEFAULT_HEADER_DEVICE_ID);
        if (StringUtils.isBlank(deviceId)) {
            throw new AppSecretException("设备id参数不能为空");
        }
        return "imooc:security:social.connect." + deviceId;
    }
}

改造相关代码处,使用写好的工具类

package cn.mrcode.imooc.springsecurity.securityapp;

import cn.mrcode.imooc.springsecurity.securityapp.social.AppSignUpUtils;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

import javax.servlet.http.HttpServletRequest;

/**
 * 处理登录的控制器
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/8 23:56
 */
@RestController
public class AppSecurityController {
    private Logger logger = LoggerFactory.getLogger(getClass());
    @Autowired
    private ProviderSignInUtils providerSignInUtils;

    @Autowired
    private AppSignUpUtils appSignUpUtils;

    @GetMapping(value = "/social/signUp")
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ConnectionData signUp(HttpServletRequest request) {
        Connection<?> connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
        // 这里还不能直接放 Connection 因为这个里面包含了很多对象
        ConnectionData connectionData = connection.createData();
        logger.info(ReflectionToStringBuilder.toString(connection, ToStringStyle.JSON_STYLE));
        appSignUpUtils.saveConnection(new ServletWebRequest(request), connectionData);
        // 注意:如果真的在客户端无session的情况下,这里是复发获取到providerSignInUtils中的用户信息的
        // 因为302重定向,是客户端重新发起请求,如果没有cookie的情况下,就不会有相同的session
        // 教程中这里应该是一个bug
        // 为了进度问题,先默认可以获取到
        // 最后要调用这一步:providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
        // 那么在demo注册控制器中这一步之前,就要把这里需要的信息获取到
        // 跟中该方法的源码,转换成使用redis存储
        return connectionData;
    }
}

注册的地方也要更改

com.example.demo.web.controller.UserController#regist
@PostMapping("/regist")
public void regist(User user, HttpServletRequest request) {

    //不管是注册用户还是绑定用户,都会拿到一个用户唯一标识。
    String userId = user.getUsername();
    appSignUpUtils.doPostSignUp(userId, new ServletWebRequest(request));
}

测试

测试时候需要不停的在浏览器和app之间切换
这里把qq登录页的地址复制下来。可以在项目关闭下扫码登录后,再启动项目,把code拿到工具中继续接下来的流程
https://graph.qq.com/oauth2.0/show?which=Login&display=pc&client_id=101316278&response_type=code&redirect_uri=http%3A%2F%2Fmrcode.cn%2Fauth%2Fqq&state=03ff3841-295a-4b03-8bbf-36ef353c146a

获取到code后,用工具访问以下地址,(如果设置了自动跳转302)则不需要再手动访问一次 /social/signUp了

如果手动访问/social/signUp的话,还是在刚在那个窗口访问,因为有相同的sessionId;

需要带上client和设备id信息

GET /auth/qq?code=D93FEF61930FCCC3C0339935B70B1215&amp;state=03ff3841-295a-4b03-8bbf-36ef353c146a HTTP/1.1
Host: mrcode.cn
Authorization: Basic bXlpZDpteWlk
deviceId: 1

返回用户信息后,提交注册用户,完成绑定第三方登录的账户

POST /user/regist HTTP/1.1
Host: mrcode.cn
Authorization: Basic bXlpZDpteWlk
deviceId: 1
Cache-Control: no-cache
Postman-Token: 4195a53a-8d2c-4417-94ec-b1252f9e5285
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"

admin
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="password"

123456