SpringSecurityOAuth简介
传统方式:基于session
- 开发繁琐
- 基于cookie:传统方式是容器和浏览器自动处理的cookie
- 安全性和客户体验差
- 有些前端技术不支持cookie,如小程序
基于token方式:oauth
- 参数中携带token
- 可以对token更大程度的控制
SpringSecurityOAuth封装了服务提供商大部分的操作;而social则是封装了客户端和服务提供商交互的流程
协议中没有规定token要怎么生成和存储。spring oath中规定了;
除了4种的标准模式;让我们自己的自定义验证也添加到该流程中,相当于自定义认证?
本章内容简介:
- 实现一个标准的OAth2协议中的Provider角色的主要功能
- 重构之前的三种认证方式的代码,使其支持token
- 高级特性
实现标准的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
授权码模式-授权
由于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信息没有用户信息,所以和前面的不一样
客户端模式
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即可; 有时间了查看源码找这些吧
思路
- 提交登录请求
- 登录成功之后,需要在上图AuthenticationSuccessHandler中获取相关信息
- 前提条件:必须携带basic client信息(因为需要它获取clientDetails信息)
- 然后走后面的逻辑
处理登录后的逻辑
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
}
重构短信登录
现有问题:
- 浏览器中使用session存储验证码
- app中午cookie概念(无session)
解决方案:
- app发送和验证 验证码必须携带一个deviceId (设备id)
- 浏览器按之前的逻辑走
也就是说,这里只是验证码的存储发生了变化,那么抽出来一个存储接口,浏览器和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生成逻辑。最后返回
测试思路:
- demo引用浏览器环境
- 使用浏览器登录后,得到code
- 使用工具发送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&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
重构注册逻辑
在浏览器中的第三方登录回顾:
- social 在拿到用户信息之后
- 查询数据库没有绑定的用户会跳转到默认的/signUp路径
- 提供了一个我们自己的注册页面,拿到用户提交的注册信息,调用social数据库服务,把关联信息写入数据库中。完成注册
- 再次登录,数据库中有用户信息,则登录成功
问题:
- 上面这个流程问题所在就是 第三方的信息存放在了 session 中;
- 还有一个问题,就是第2步会302.需要客户端信息判定并跳转到登录页
所以现在开始改造,改造方案:
- 流程完成后,更改跳转的页面到app指定页面,
- 根据设备id,我们把信息存放在redis中
- 用户注册完成后,提交,再把第三方信息拿出来,合并完成注册
改造
注意: 在改造测试之前把默认注册用户的功能关闭掉
也就是 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&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
本文由 liyunfei 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Jul 25,2022