SpringSecurity实战笔记之Social

发布时间 2023-08-18 17:33:48作者: 咔咔皮卡丘

===================Spring Social================

一、OAuth协议:

在不向第三方应用提供账号、密码的情况下,允许其访问资源所有者特定资源所使用的协议,例如微信授权登录。
最常用的有 授权码模式、密码模式


二、Spring Social基本原理:

1、SocialAuthenticationFilter将其拦截下来,并引导其完全OAuth所需的流程,然后根据用户信息构建Authentication并放入SecurityContext中;
2、授权码模式流程(A为资源所有者,B为服务提供商(同时拥有认证服务器B1,资源服务器B2),C为第三方应用):
A访问C->
(1)、C将A导向B1->
(2)、B1询问A是否同意授权->A回应B1同意->
(3)、B1携带授权码回到C->
(4)、C拿到授权码后向B1申请住令牌->
(5)、B1向C发放令牌->
(6)、C拿着令牌向B2获取用户资源
(7)、根据用户信息构建Authentication并放入SecurityContext中
(1)到(5)是OAuth协议标准流程,(6)为个性化流程
3、要实现的接口和类
3.1、与(1)到(6)步相关的接口与类
ServiceProvider(Spring Social 提供了抽象类AbstractOAuth2ServiceProvider)
OAuth2Operations(Spring Social 提供了OAuth2Template)完成(1)到(5)是OAuth协议标准流程
Api(Spring Social 提供了抽象类AbstractOAuth2ApiBinding)完成(6)所需要的接口实现
3.2、与(7)步相关的接口与类
-Connection(实现类OAuth2Connection,有固定的数据结构)包含用户信息的对象,由ConnectionFactory创建
-ConnectionFactory(实现类OAuth2ConnectionFactory)完成将获取到的用户信息转化为Connection数据结构,包含着
ServiceProvider实现
ApiAdapter实现
-UersConnectionRepository(实现类JdbcUsersConnectionRepository)操作DB UserConnection表(保存着业务表中userId与第三方用户信息对应记录)
三、QQ登录
1、QQ互联api:http://wiki.connect.qq.com/get_user_info
2、Api实现
2.1、 接口
public interface QQ
2.2、接口的实现并继承了AbstractOAuth2ApiBinding
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ
2.3、用来接收userInfo的对象
public class QQUserInfo
3、ServiceProvider实现
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ>
4、ApiAdapter实现
public class QQAdapter implements ApiAdapter<QQ>
5、ConnectionFactory实现
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ>
6、配置(相关参数放在QQProperties类中)
6.1、QQAutoConfig
@Configuration
@ConditionalOnProperty(prefix = "imooc.security.social.qq",name = "app-id")
public class QQAutoConfig extends SocialAutoConfigurerAdapter {

@Autowired
private SecurityProperties securityProperties;
@Override
protected ConnectionFactory<?> createConnectionFactory() {
QQProperties qqConfig = securityProperties.getSocial().getQq();
return new QQConnectionFactory(qqConfig.getProviderId(),qqConfig.getAppId(),qqConfig.getAppSecret());
}
}

6.2、SocialConfig(与weixin共用)
@Configuration
@EnableSocial
@Order(Integer.MIN_VALUE)//UsersConnectionRepository的实现类是通过取SocialConfigurer实现类list中的第一个得到,故要对想要的实现类加上Order(Integer.MIN_VALUE)
public class SocialConfig extends SocialConfigurerAdapter{

@Autowired
private DataSource dataSource;

//ConnectionFactoryLocator 作用是查找ConnectionFactory,并判断使用哪个ConnectionFactory
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
repository.setTablePrefix("imooc_");
if(connectionSignUp!=null){
repository.setConnectionSignUp(connectionSignUp);
}
return repository;
}


@Bean
public SpringSocialConfigurer imoocSocialSecurityConfig(){
return new SpringSocialConfigurer();
}
}

7、将imoocSocialSecurityConfig apply到BrowserSecurityConfig中
8、SocialUserDetailsService的实现
@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
logger.info("社交登录用户ID:"+userId);
return buildUser(userId);
}

private SocialUserDetails buildUser(String userId) {
String password = passwordEncoder.encode("123456");
logger.info("数据库密码是:"+password);
return new SocialUser(userId,password, true,true,true,true,AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN"));
}
9、在登录页面添加QQ登录链接<a href="/auth/qq">QQ登录</a>
9.1、/auth 是由SocialAuthenticationFilter中控制的,可以通过重写SpringSocialConfigure的postProcess方法进行自定义
9.2、/qq 是第三方商户id(providerId),完全是自定义
10、要注意地方
10.1、请求QQ授权后返回的content-type是html,但spring security默认期望返回为json格式的字符串,并通过解析json字符串得到返回的值
这件事是由OAuth2Template.createRestTemplate方法实例出来的RestTemplate做的,故要创建QQOAuth2Template(QQServiceProvider中要改为采用QQOAuth2Template)替换掉OAuth2Template.createRestTemplate的实现,让其支持html,并从返回的字符串中读取对应的字段
代码:
public class QQOAuth2Template extends OAuth2Template {

Logger logger = LoggerFactory.getLogger(getClass());

public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
setUseParametersForClientAuthentication(true);//设为true后,将会将client_id、client_secret传过去
}

//添加支持html content-type
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}

//从返回的字符串中读取对应的字段
@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
//access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
logger.info("获accessToken的响应:"+responseStr);
String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");
String accessToken = StringUtils.substringAfterLast(items[0],"=");
Long expireIn = new Long(StringUtils.substringAfterLast(items[1],"="));
String refreshToken = StringUtils.substringAfterLast(items[2],"=");
return new AccessGrant(accessToken,null,refreshToken,expireIn);
}
}
四、注册
1、SocialAuthenticationProvider 对 SocialAuthenticationToken 判断userId有没有值,就抛出BadCredentialsException,由SocialAuthenticationFilter进行处理:将信息保存在session中在抛出SocialAuthenticationRedirectException跳到注册页面
2、自定义注册页并使得路径可配置
application.properties中
imooc.security.browser.signUpUrl = demo-signUp.html

@Bean
public SpringSocialConfigurer imoocSocialSecurityConfig(){
//使用默认的配置,拦截路径为/auth
//return new SpringSocialConfigurer();
//自定义配置
String filterProcessUrl = securityProperties.getSocial().getFilterProcessUrl();
ImoocSpringSocialConfigure configure = new ImoocSpringSocialConfigure(filterProcessUrl);
configure.signupUrl(securityProperties.getBrowser().getSignUpUrl());//自定义注册页面
return configure;
}
3、ProviderSignInUtils工具类
//读写缓存了第三方登录信息的工具类
@Autowired
private ConnectionFactoryLocator connectionFactoryLocator;
@Bean
public ProviderSignInUtils providerSignInUtils(){
return new ProviderSignInUtils(connectionFactoryLocator,getUsersConnectionRepository(connectionFactoryLocator));
}

然后就可以在controller中使用providerSignInUtils读写imooc_userconnection表中的数据

@Autowired
private ProviderSignInUtils providerSignInUtils;

//获取SocialUserInfo
@GetMapping("/social/user")
public SocialUserInfo getSocialUserInfo(HttpServletRequest request){
SocialUserInfo userInfo = new SocialUserInfo();
Connection<?> connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
userInfo.setProviderId(connection.getKey().getProviderId());
userInfo.setProviderUserId(connection.getKey().getProviderUserId());
userInfo.setHeadimg(connection.getImageUrl());
userInfo.setNickname(connection.getDisplayName());
return userInfo;
}

//更新imooc_userconnection
@PostMapping("/regist")
public Object regist(User user,HttpServletRequest request){
//不管是注册用户还是绑定用户,都会拿到用户的唯一标识
String userName = user.getUsername();
providerSignInUtils.doPostSignUp(userName,new ServletWebRequest(request));
return user;
}
4、偷偷地给用户注册一个用户
实现JdbcUsersConnectionRepository的ConnectionSignUp,就可以自动注册一个用户,不必跳到注册页面d
@Component
public class DemoConnectionSignUp implements ConnectionSignUp {
@Override
public String execute(Connection<?> connection) {
//根据社交用户信息默认创建用户并返回用户唯一标识
return connection.getDisplayName();
}
}
安全模块中配置
@Autowired(required = false)
private ConnectionSignUp connectionSignUp;

@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
repository.setTablePrefix("imooc_");
if(connectionSignUp!=null){
repository.setConnectionSignUp(connectionSignUp);
}
return repository;
}

五、微信登录(与QQ登录相似,下面只说明一下不同点)
1、api:WeixinImpl的getUserInfo方法要传入openId,重写getMessageConverters()方法
/**
* 默认注册的StringHttpMessageConverter字符集为ISO-8859-1,而微信返回的是UTF-8的,所以覆盖了原来的方法。
*/
@Override
protected List<HttpMessageConverter<?>> getMessageConverters() {
List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters();
messageConverters.remove(0);
messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return messageConverters;
}
2、OAuth2Template:WeixinOAuth2Template重构了与请求url,返回结果解析相关方法,因为微信有些参数名与标准的OAuth2.0不同,详情查看代码
exchangeForAccess、refreshAccess、buildAuthenticateUrl、buildAuthorizeUrl
3、ConnectionFactory:与QQ相差很大,由于微信的openId是和accessToken一起返回的,需要重写以下方法
extractProviderUserId、createConnection,而且ApiAdapter类要每次调用都new一个,不能共用
六、绑定与解绑(此处有问题java.lang.IllegalAccessError)
1、ConnectController:提供了/connect接口,将用户的绑定情况add到Model返回的是connect/status视图。我们需要写一个connect/status view
@Component("connect/status")
public class ImoocConnectionStatusView extends AbstractView{

@Autowired
private ObjectMapper objectMapper;

@SuppressWarnings("unchecked")
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {

Map<String, List<Connection<?>>> connections = (Map<String, List<Connection<?>>>) model.get("connectionMap");

Map<String, Boolean> result = new HashMap<>();
for (String key : connections.keySet()) {
result.put(key, CollectionUtils.isNotEmpty(connections.get(key)));
}

response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(result));
}
}
2、对社交账号进行绑定与解绑
使用post方式请求/connect/weixin 进行绑定,使用delete方式请求/connect/weixin 进行解绑,同时实现返回视图ImoocConnectView(因为此视图想做共用,所以不能直接使用@Component("connect/weixin"))
public class ImoocConnectView extends AbstractView {

@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {

response.setContentType("text/html;charset=UTF-8");
if (model.get("connection") == null) {
response.getWriter().write("<h3>解绑成功</h3>");
} else {
response.getWriter().write("<h3>绑定成功</h3>");
}
}
}

WeixinAutoConfiguration中指定绑定成功与解绑成功的视图
@Bean({"connect/weixinConnect", "connect/weixinConnected"})
@ConditionalOnMissingBean(name = "weixinConnectedView")
public View weixinConnectedView() {
return new ImoocConnectView();
}
七、单机session管理
1、application.properties配置
#单位为秒,最短>60,对应代码TomcatEmbeddedServletContainerFactory类中的getSessionTimeoutInMinutes方法
server.session.timeout= 60
2、session失效时,需要特殊提示
2.1、在BrowserSecurityConfig中添加
.sessionManagement()
.invalidSessionUrl("/session/invalid")
.and()
2.2、视图
@GetMapping("/session/invalid")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)//401
public SimpleResponse sessionInvalid(){
String message = "session 失效";
return new SimpleResponse(message);
}
2.3、将"/session/invalid"配置为不需要权限认证就能访问
3、session并发控制(最常见的场景控制一个用户只有一个session)
3.1 在BrowserSecurityConfig中添加
.sessionManagement()
.invalidSessionUrl("/session/invalid")
.maximumSessions(1)
.maxSessionsPreventsLogin(true)//当达到最大并发session数后阻止登录(默认为false)
.expiredSessionStrategy(new ImoocExpiredSessionStrategy())
.and()
.and()
3.2 expiredSessionStrategy策略
public class ImoocExpiredSessionStrategy implements SessionInformationExpiredStrategy{
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
event.getResponse().setContentType("application/json;charset=UTF-8");

event.getResponse().getWriter().write("{\"message\":\"并发登录\"}");
}
}
3.3、如果是自定义的user,需要重写user的toString,hashCode,equals方法
@Override
public String toString() {
return this.username;
}

@Override
public int hashCode() {
return username.hashCode();
}

@Override
public boolean equals(Object obj) {
return this.toString().equals(obj.toString());
}
4、重构
4.1、BrowserSecurityConfig修改
@Autowired
private InvalidSessionStrategy invalidSessionStrategy;

@Autowired
private ValidateCodeSecurityConfig validateCodeSecurityConfig;

.sessionManagement()
.invalidSessionStrategy(invalidSessionStrategy)
.maximumSessions(securityProperties.getBrowser().getSession().getMaximumSessions())
.maxSessionsPreventsLogin(securityProperties.getBrowser().getSession().isMaxSessionsPreventsLogin())//当达到最大并发session数后阻止登录
.expiredSessionStrategy(sessionInformationExpiredStrategy)
.and()
.and()
4.2、InvalidSessionStrategy、ValidateCodeSecurityConfig类实现extends AbstractSessionStrategy implements SessionInformationExpiredStrategy并注入容器
在BrowserSecurityBeanConfig中
@Autowired
private SecurityProperties securityProperties;

/**
* session失效时的处理策略配置
* @return
*/
@Bean
@ConditionalOnMissingBean(InvalidSessionStrategy.class)
public InvalidSessionStrategy invalidSessionStrategy(){
return new ImoocInvalidSessionStrategy(securityProperties);
}

/**
* 并发登录导致前一个session失效时的处理策略配置
* @return
*/
@Bean
@ConditionalOnMissingBean(SessionInformationExpiredStrategy.class)
public SessionInformationExpiredStrategy sessionInformationExpiredStrategy(){
return new ImoocExpiredSessionStrategy(securityProperties);
}
4.3 AbstractSessionStrategy类
八、集群session管理
1、session支持的存储类型,可以查看StoreType: REDIS,MONGO,JDBC,HAZELCAST,HASH_MAP,NONE;
2、使用redis:spring.session.store-type=redis
#spring.redis.host=localhost
#spring.redis.port=6379
3、存入redis中的对象(包括对象中的对象)都要序列化:原来的ValidateCode、ImageCode对象需要序列化
同时,ImageCode对象中BufferedImage对象并没有实现序列化,故存入redis中时,不要给BufferedImage赋值
4、使用了redis保存session后,超时与最大并发session数的功能一样生效

九、退出登录
1、默认路径:/logout
2、默认的处理逻辑
2.1、使当前session失效
2.2、清除与当前用户相关的remember-me记录
2.3、清空当前的SecurityContext
2.4、重定向到登录页面
3、配置
3.1、指定退出路径,退出成功后重定向路径,清除cookie

@Autowired
private LogoutSuccessHandler logoutSuccessHandler;

.logout()
.logoutUrl("/signOut")
//.logoutSuccessUrl("/imooc-logout.html")//此路径要添加到允许放行中
.logoutSuccessHandler(logoutSuccessHandler)
.deleteCookies("JSESSIONID")
.and()

3.2、LogoutSuccessHandler类
/**
* 默认的退出成功处理器,如果设置了imooc.security.browser.signOutUrl,则跳到配置的地址上,
* 如果没配置,则返回json格式的响应。
*
* @author zhailiang
*
*/
public class ImoocLogoutSuccessHandler implements LogoutSuccessHandler {

private Logger logger = LoggerFactory.getLogger(getClass());

public ImoocLogoutSuccessHandler(String signOutSuccessUrl) {
this.signOutSuccessUrl = signOutSuccessUrl;
}

private String signOutSuccessUrl;

private ObjectMapper objectMapper = new ObjectMapper();

@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {

logger.info("退出成功");

if (StringUtils.isBlank(signOutSuccessUrl)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse("退出成功")));
} else {
response.sendRedirect(signOutSuccessUrl);
}
}
}
3.3、在BrowserSecurityBeanConfig类中将ImoocLogoutSuccessHandler注入容器中
@Bean
@ConditionalOnMissingBean(LogoutSuccessHandler.class)
public LogoutSuccessHandler logoutSuccessHandler(){
return new ImoocLogoutSuccessHandler(securityProperties.getBrowser().getSignOutUrl());
}