前言
博客主要分享如何实现QQ与微信登录
一、OAuth协议
1.OAuth协议要解决的问题
可以帮我们存在问题:
- 应用可以访问用户在微信上的所有数据
- 用户只有修改密码,才能收回授权
- 密码泄露的可能性大大提高
2.OAuth协议中的各种角色
3.OAuth协议运行流程
- 授权模式分为:
- 授权码模式流程:
二、SpringSocial基本原理:
图一:
我们的SpringSocial把上面的流程封装到了如下图所示的过滤器中:
针对图一,我们需要实现如下接口进行操作:
https://spring.io/projects/spring-social#overview
https://spring.io/blog/2018/07/03/spring-social-end-of-life-announcement
三、开发QQ登录
package com.zcw.security.core.social.qq.api;
import java.io.IOException;
public interface QQ {
/**
* 获取用户信息
*/
QQUserinfo getUserInfo() throws IOException;
}
- 创建对象
package com.zcw.security.core.social.qq.api;
import lombok.Data;
/**
* @ClassName : QQUserinfo
* @Description :返回参数:https://wiki.connect.qq.com/get_user_info
* @Author : Zhaocunwei
* @Date: 2020-06-28 13:29
*/
@Data
public class QQUserinfo {
/**
* 返回码
*/
private String ret;
/**
* 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。
*/
private String msg;
/**
* 用户在QQ空间的暱称。
*/
private String nickname;
/**
* 大小为30×30像素的QQ空间头像URL。
*/
private String figureurl;
/**
* 大小为50×50像素的QQ空间头像URL。
*/
private String figureurl_1;
/**
* 大小为100×100像素的QQ空间头像URL。
*/
private String figureurl_2;
/**
* 大小为40×40像素的QQ头像URL。
*/
private String figureurl_qq_1;
/**
* 大小为100×100像素的QQ头像URL。
* 需要注意,不是所有的用户都拥有QQ的100x100的头像,但40x40像素则是一定会有。
*/
private String figureurl_qq_2;
/**
* 性别。 如果获取不到则默认返回"男"
*/
private String gender;
}
- 获取用户信息
package com.zcw.security.core.social.qq.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang.StringUtils;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.TokenStrategy;
import java.io.IOException;
/**
* @ClassName : QQImpl
* @Description : 此类不能交给spring容器管理,因为不是单例
* @Author : Zhaocunwei
* @Date: 2020-06-28 13:30
*/
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
private static final String URL_GET_OPENID="https://graph.qq.com/oauth2.0/me?access_token=%s";
private static final String URL_GET_USERINFO="https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";
private String appId;
private String openId;
private ObjectMapper objectMapper = new ObjectMapper();
public QQImpl(String accessToken,String appId){
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
this.appId=appId;
String url =String.format(URL_GET_OPENID,accessToken);
String result = getRestTemplate().getForObject(url,String.class);
System.out.println(result);
this.openId= StringUtils.substringBetween(result,"\"openid\":","}");
}
@Override
public QQUserinfo getUserInfo() throws IOException {
String url = String.format(URL_GET_USERINFO,appId,openId);
String result =getRestTemplate().getForObject(url,String.class);
System.out.println(result);
return objectMapper.readValue(result,QQUserinfo.class);
}
}
package com.zcw.security.core.social.qq.connet;
import com.zcw.security.core.social.qq.api.QQ;
import com.zcw.security.core.social.qq.api.QQUserinfo;
import lombok.SneakyThrows;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;
/**
* @ClassName : QQAdapter
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-28 14:39
*/
public class QQAdapter implements ApiAdapter<QQ> {
@Override
public boolean test(QQ qq) {
return true;
}
@SneakyThrows //实际开发中需要我们自己,捕获异常,进行相关业务处理
@Override
public void setConnectionValues(QQ qq, ConnectionValues connectionValues) {
QQUserinfo userinfo = qq.getUserInfo();
connectionValues.setDisplayName(userinfo.getNickname());
connectionValues.setImageUrl(userinfo.getFigureurl_qq_1());
connectionValues.setProfileUrl(null);
connectionValues.setProviderUserId(userinfo.getOpentId());
}
@Override
public UserProfile fetchUserProfile(QQ qq) {
return null;
}
@Override
public void updateStatus(QQ qq, String s) {
}
}
package com.zcw.security.core.social.qq.connet;
import com.zcw.security.core.social.qq.api.QQ;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
/**
* @ClassName : QQConnectionFactory
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-28 14:47
*/
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
public QQConnectionFactory(String providerId, String appId,String appSecret) {
super(providerId, new QQServiceProvider(appId,appSecret), new QQAdapter());
}
}
create table UserConnection (userId varchar(255) not null,
providerId varchar(255) not null,
providerUserId varchar(255),
rank int not null,
displayName varchar(255),
profileUrl varchar(512),
imageUrl varchar(512),
accessToken varchar(512) not null,
secret varchar(512),
refreshToken varchar(512),
expireTime bigint,
primary key (userId, providerId, providerUserId));
create unique index UserConnectionRank on UserConnection(userId, providerId, rank);
- 修改我们用户登录,service
package com.zcw.security.browser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.social.security.SocialUser;
import org.springframework.social.security.SocialUserDetails;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component;
/**
* @ClassName : MyUserDetailsService
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-18 18:31
*/
@Component
@Slf4j
public class MyUserDetailsService implements UserDetailsService, SocialUserDetailsService {
//模拟从数据库中获取加密的数据
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("登录用户名:"+username);
//根据用户名查找用户信息
// return new User(username,
// //在数据库中存的密码
// "123456",
// //数据库中权限
// AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
//根据查找到的用户信息判断用户是否被冻结
String password = passwordEncoder.encode("123456");
log.info("数据库密码是:"+password);
return new User(username,password,
true,true,true,false,
AuthorityUtils.commaSeparatedStringToAuthorityList("amind"));
}
@Override
public SocialUserDetails loadUserByUserId(String userId)
throws UsernameNotFoundException {
return new SocialUser(userId,passwordEncoder.encode("123456")
,true,true,true,true,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
- 配置信息
package com.zcw.security.core.properties;
import lombok.Data;
import org.springframework.boot.autoconfigure.social.SocialProperties;
/**
* @ClassName : QQProperties
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-28 15:18
*/
@Data
public class QQProperties extends SocialProperties {
private String providerId="qq";
}
package com.zcw.security.core.properties;
import lombok.Data;
/**
* @ClassName : SocialProperties
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-28 15:20
*/
@Data
public class SocialProperties {
private QQProperties qqProperties = new QQProperties();
}
- 放入全局
package com.zcw.security.core.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @ClassName : SecurityProperties
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-19 13:54
*/
@ConfigurationProperties(prefix = "zcw.security")
@Component
public class MySecurityProperties {
private BrowserProperties browserProperties = new BrowserProperties();
private ValidateCodeProperties validateCodeProperties = new ValidateCodeProperties();
private SocialProperties socialProperties = new SocialProperties();
public BrowserProperties getBrowserProperties() {
return browserProperties;
}
public void setBrowserProperties(BrowserProperties browserProperties) {
this.browserProperties = browserProperties;
}
public ValidateCodeProperties getValidateCodeProperties() {
return validateCodeProperties;
}
public void setValidateCodeProperties(ValidateCodeProperties validateCodeProperties) {
this.validateCodeProperties = validateCodeProperties;
}
public SocialProperties getSocialProperties() {
return socialProperties;
}
public void setSocialProperties(SocialProperties socialProperties) {
this.socialProperties = socialProperties;
}
}
package com.zcw.security.core.social.qq.config;
import com.zcw.security.core.properties.MySecurityProperties;
import com.zcw.security.core.properties.QQProperties;
import com.zcw.security.core.social.qq.connet.QQConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter;
import org.springframework.context.annotation.Configuration;
import org.springframework.social.connect.ConnectionFactory;
/**
* @ClassName : QQAutoConfig
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-28 15:24
*/
@Configuration
@ConditionalOnProperty(prefix="zcw.security.social.qq",name = "app-id")
public class QQAutoConfig extends SocialAutoConfigurerAdapter {
@Autowired
private MySecurityProperties mySecurityProperties;
@Override
protected ConnectionFactory<?> createConnectionFactory() {
QQProperties qqProperties = mySecurityProperties.getSocialProperties().getQqProperties();
return new QQConnectionFactory(
qqProperties.getProviderId(),
qqProperties.getAppId(),
qqProperties.getAppSecret()
);
}
}
- demo项目中进行配置文件的配置
zcw:
security:
social:
qq:
app-id: xxxx
app-secret: xxxxx
- 添加过滤器链
- 测试
- 解决上面报错问题:
需要我们添加网站的回调域. - 修改我们本地hosts文件
如果配置域名以后默认是访问80端口,需要我们做映射,指向我们服务器,端口
package com.zcw.security.core.social;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;
/**
* @ClassName : ZcwSpringSocialConfigurer
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-28 18:17
*/
public class ZcwSpringSocialConfigurer extends SpringSocialConfigurer {
private String filterProcessesUrl;
public ZcwSpringSocialConfigurer(String filterProcessesUrl){
this.filterProcessesUrl = filterProcessesUrl;
}
/**
* object 就是我们放到过滤器链上的filter
* @param object
* @param <T>
* @return
*/
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
return (T) filter;
}
}
- 修改配置类,把之前new 对象写成我们自定义的
- 修改配置文件映射类:
package com.zcw.security.core.social;
import com.zcw.security.core.properties.MySecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.config.annotation.EnableSocial;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;
import org.springframework.social.security.SpringSocialConfigurer;
import javax.sql.DataSource;
import javax.xml.ws.soap.Addressing;
/**
* @ClassName : SocialConfig
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-28 14:51
*/
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private MySecurityProperties mySecurityProperties;
@Override
public UsersConnectionRepository getUsersConnectionRepository(
ConnectionFactoryLocator connectionFactoryLocator) {
return new JdbcUsersConnectionRepository(
dataSource,
connectionFactoryLocator,
Encryptors.noOpText()
);
}
@Bean
public SpringSocialConfigurer zcwSocialSecurityConfig(){
String filterPrpcessesUrl = mySecurityProperties.getSocialProperties().getFilterPrpcessesUrl();
ZcwSpringSocialConfigurer configurer = new ZcwSpringSocialConfigurer(filterPrpcessesUrl);
return configurer;
}
}
- 修改配置文件,这样请求地址完全是可以配置的
- Spring Social开发第三方登录:
- 自定义QQ 请求格式
package com.zcw.security.core.social.qq;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2Template;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.Charset;
/**
* @ClassName : QQOAuh2Template
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-29 10:16
*/
@Slf4j
public class QQOAuh2Template extends OAuth2Template {
public QQOAuh2Template(String clientId, String clientSecret,
String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
setUseParametersForClientAuthentication(true);
}
public QQOAuh2Template(String clientId, String clientSecret,
String authorizeUrl, String authenticateUrl,
String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, authenticateUrl, accessTokenUrl);
}
@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl,
MultiValueMap<String, String> parameters) {
String responseStr = getRestTemplate().postForObject(accessTokenUrl,
parameters,
String.class);
log.info("获取accessToken的响应:{}",responseStr);
String [] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr,"&");
String accessToken =StringUtils.substringAfterLast(items[0],"=");
Long expiresIn = new Long(StringUtils.substringAfterLast(items[1],"="));
String refreshToken = StringUtils.substringAfter(items[2],"=");
return new AccessGrant(accessToken,null,refreshToken,expiresIn);
}
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate =super.createRestTemplate();
restTemplate.getMessageConverters().add(
new StringHttpMessageConverter(Charset.forName("UTF-8"))
);
return restTemplate;
}
}
- 修改我们自定义的ServiceProvider类
package com.zcw.security.core.social.qq.connet;
import com.zcw.security.core.social.qq.QQOAuh2Template;
import com.zcw.security.core.social.qq.api.QQ;
import com.zcw.security.core.social.qq.api.QQImpl;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;
import org.springframework.social.oauth2.OAuth2Template;
/**
* @ClassName : QQServiceProvider
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-28 14:11
*/
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {
private String appId;
private static final String URL_AUTHORIZE="https://graph.qq.com/oauth2.0/authorize";
private static final String URL_ACCESS_TOKEN="https://graph.qq.com/oauth2.0/token";
public QQServiceProvider(String appId,String appSecret) {
super(new QQOAuh2Template(appId,appSecret,URL_AUTHORIZE,URL_ACCESS_TOKEN));
}
@Override
public QQ getApi(String accessToken) {
return new QQImpl(accessToken,appId);
}
}
- 测试:
- 修改
处理注册逻辑
- 修改配置文件
- 修改配置类
- 修改过滤器,
当我们找不到用户时,进行一些响应的处理
- 配置工具类
package com.zcw.security.core.social;
import com.zcw.security.core.properties.MySecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.config.annotation.EnableSocial;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.social.security.SpringSocialConfigurer;
import javax.sql.DataSource;
import javax.xml.ws.soap.Addressing;
/**
* @ClassName : SocialConfig
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-28 14:51
*/
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private MySecurityProperties mySecurityProperties;
@Override
public UsersConnectionRepository getUsersConnectionRepository(
ConnectionFactoryLocator connectionFactoryLocator) {
return new JdbcUsersConnectionRepository(
dataSource,
connectionFactoryLocator,
Encryptors.noOpText()
);
}
@Bean
public SpringSocialConfigurer zcwSocialSecurityConfig(){
String filterPrpcessesUrl = mySecurityProperties.getSocialProperties().getFilterPrpcessesUrl();
ZcwSpringSocialConfigurer configurer = new ZcwSpringSocialConfigurer(filterPrpcessesUrl);
configurer.signupUrl(mySecurityProperties.getBrowserProperties()
.getSingUpUrl());
return configurer;
}
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator){
return new ProviderSignInUtils(
connectionFactoryLocator,getUsersConnectionRepository(connectionFactoryLocator));
}
}
- 编写我们的controller层
package com.zcw.security.browser.support;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
/**
* @ClassName : SocialUserInfo
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-29 11:18
*/
@Data
public class SocialUserInfo {
private String providerId;
private String providerUserId;
private String nickname;
private String headimg;
}
- 修改我们的注册方法:
- 这个注册方法,也是不需要登录可以访问,所以需要添加权限
- 实现 ConnectionSingUp
package com.zcw.security;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.stereotype.Component;
/**
* @ClassName : DemoConnectionSignUp
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-29 12:47
*/
@Component
public class DemoConnectionSignUp implements ConnectionSignUp {
@Override
public String execute(Connection<?> connection) {
//根据社交用户信息默认创建用户并返回用户唯一标识
return connection.getDisplayName();
}
}
- 修改我们安全模块
四、微信登录
- 创建properties类
package com.zcw.security.core.properties;
import org.springframework.boot.autoconfigure.social.SocialProperties;
import lombok.Data;
/**
* @ClassName : WeixinProperties
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-29 13:10
*/
@Data
public class WeixinProperties extends SocialProperties {
/**
* 第三方id,用来决定发起第三方登录的URL,
* 默认是weixin.
*/
private String providerId="weixin";
}
- 修改demo 配置文件,配置微信相关信息
- 封装微信相关接口
package com.zcw.security.core.social.weixin.api;
/**
* @ClassName : WeixinUserProfile
* @Description : 微信用户信息
* @Author : Zhaocunwei
* @Date: 2020-06-29 13:39
*/
import lombok.Data;
@Data
public class WeixinUserInfo{
/**
* 普通用户的标识,对当前开发者账号唯一
*/
private String openid;
/**
* 普通用户暱称
*/
private String nickname;
/**
* 语言
*/
private String language;
/**
* 普通用户性别: 1为男,2为女
*/
private String sex;
/**
* 普通用户个人资料填写的省份
*/
private String province;
/**
* 普通用户个人资料填写的城市
*/
private String city;
/**
* 国家,如中国为CN
*/
private String country;
/**
* 用户头像
*/
private String headimgurl;
/**
* 用户特权信息,json数组,如微信沃卡用户为chinaunicom
*/
private String[] privilege;
/**
* 用户统一标识,针对一个微信开发平台账号下的应用,
* 同一用户的unionid是唯一的
*/
private String unionid;
}
package com.zcw.security.core.social.weixin.api;
import java.nio.charset.Charset;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.TokenStrategy;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @ClassName : WeixinImpl
* @Description :微信API调用模板
* @Author : Zhaocunwei
* @Date: 2020-06-29 13:54
*/
public class WeixinImpl extends AbstractOAuth2ApiBinding implements Weixin {
/**
*
*/
private ObjectMapper objectMapper = new ObjectMapper();
/**
* 获取用户信息的url
*/
private static final String URL_GET_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?openid=";
/**
* @param accessToken
*/
public WeixinImpl(String accessToken) {
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
}
/**
* 默认注册的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;
}
/**
* 获取微信用户信息。
*/
@Override
public WeixinUserInfo getUserInfo(String openId) {
String url = URL_GET_USER_INFO + openId;
String response = getRestTemplate().getForObject(url, String.class);
if(StringUtils.contains(response, "errcode")) {
return null;
}
WeixinUserInfo profile = null;
try {
profile = objectMapper.readValue(response, WeixinUserInfo.class);
} catch (Exception e) {
e.printStackTrace();
}
return profile;
}
}
- 创建微信access_token类
package com.zcw.security.core.social.weixin.connect;
import org.springframework.social.oauth2.AccessGrant;
/**
* @ClassName : WeixinAccessGnant
* @Description :微信的access_token信息。与标准OAuth2协议不同,
* 微信在获取access_token时会同时返回openId,并没有单独的通过accessToke换取openId的服务
* * 所以在这里继承了标准AccessGrant,添加了openId字段,作为对微信access_token信息的封装。
* @Author : Zhaocunwei
* @Date: 2020-06-29 14:19
*/
public class WeixinAccessGrant extends AccessGrant {
/**
*
*/
private static final long serialVersionUID = -7243374526633186782L;
private String openId;
public WeixinAccessGrant() {
super("");
}
public WeixinAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn) {
super(accessToken, scope, refreshToken, expiresIn);
}
/**
* @return the openId
*/
public String getOpenId() {
return openId;
}
/**
* @param openId the openId to set
*/
public void setOpenId(String openId) {
this.openId = openId;
}
}
- 完成微信的OAuth2认证流程的模板类
package com.zcw.security.core.social.weixin.connect;
import java.nio.charset.Charset;
import java.util.Map;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2Parameters;
import org.springframework.social.oauth2.OAuth2Template;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @ClassName : WeixinOAuth2Template
* @Description : 完成微信的OAuth2认证流程的模板类。国内厂商实现的OAuth2每个都不同,
* spring默认提供的OAuth2Template适应不了,只能针对每个厂商自己微调。
* @Author : Zhaocunwei
* @Date: 2020-06-29 14:17
*/
public class WeixinOAuth2Template extends OAuth2Template {
private String clientId;
private String clientSecret;
private String accessTokenUrl;
private static final String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token";
private Logger logger = LoggerFactory.getLogger(getClass());
public WeixinOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
setUseParametersForClientAuthentication(true);
this.clientId = clientId;
this.clientSecret = clientSecret;
this.accessTokenUrl = accessTokenUrl;
}
/* (non-Javadoc)
* @see org.springframework.social.oauth2.OAuth2Template#exchangeForAccess(java.lang.String, java.lang.String, org.springframework.util.MultiValueMap)
*/
@Override
public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri,
MultiValueMap<String, String> parameters) {
StringBuilder accessTokenRequestUrl = new StringBuilder(accessTokenUrl);
accessTokenRequestUrl.append("?appid="+clientId);
accessTokenRequestUrl.append("&secret="+clientSecret);
accessTokenRequestUrl.append("&code="+authorizationCode);
accessTokenRequestUrl.append("&grant_type=authorization_code");
accessTokenRequestUrl.append("&redirect_uri="+redirectUri);
return getAccessToken(accessTokenRequestUrl);
}
@Override
public AccessGrant refreshAccess(String refreshToken,
MultiValueMap<String, String> additionalParameters) {
StringBuilder refreshTokenUrl = new StringBuilder(REFRESH_TOKEN_URL);
refreshTokenUrl.append("?appid="+clientId);
refreshTokenUrl.append("&grant_type=refresh_token");
refreshTokenUrl.append("&refresh_token="+refreshToken);
return getAccessToken(refreshTokenUrl);
}
@SuppressWarnings("unchecked")
private AccessGrant getAccessToken(StringBuilder accessTokenRequestUrl) {
logger.info("获取access_token, 请求URL: "+accessTokenRequestUrl.toString());
String response = getRestTemplate().getForObject(accessTokenRequestUrl.toString(), String.class);
logger.info("获取access_token, 响应内容: "+response);
Map<String, Object> result = null;
try {
result = new ObjectMapper().readValue(response, Map.class);
} catch (Exception e) {
e.printStackTrace();
}
//返回错误码时直接返回空
if(StringUtils.isNotBlank(MapUtils.getString(result, "errcode"))){
String errcode = MapUtils.getString(result, "errcode");
String errmsg = MapUtils.getString(result, "errmsg");
throw new RuntimeException("获取access token失败, errcode:"+errcode+", errmsg:"+errmsg);
}
WeixinAccessGrant accessToken = new WeixinAccessGrant(
MapUtils.getString(result, "access_token"),
MapUtils.getString(result, "scope"),
MapUtils.getString(result, "refresh_token"),
MapUtils.getLong(result, "expires_in"));
accessToken.setOpenId(MapUtils.getString(result, "openid"));
return accessToken;
}
/**
* 构建获取授权码的请求。也就是引导用户跳转到微信的地址。
*/
@Override
public String buildAuthenticateUrl(OAuth2Parameters parameters) {
String url = super.buildAuthenticateUrl(parameters);
url = url + "&appid="+clientId+"&scope=snsapi_login";
return url;
}
@Override
public String buildAuthorizeUrl(OAuth2Parameters parameters) {
return buildAuthenticateUrl(parameters);
}
/**
* 微信返回的contentType是html/text,添加相应的HttpMessageConverter来处理。
*/
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(
new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
}
- 创建微信适配器类
package com.zcw.security.core.social.weixin.connect;
import com.zcw.security.core.social.weixin.api.Weixin;
import com.zcw.security.core.social.weixin.api.WeixinUserInfo;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;
/**
* @ClassName : WeixinAdapter
* @Description : 微信 api适配器,将微信 api的数据模型转为spring social的标准模型。
* @Author : Zhaocunwei
* @Date: 2020-06-29 14:25
*/
public class WeixinAdapter implements ApiAdapter<Weixin> {
private String openId;
public WeixinAdapter() {}
public WeixinAdapter(String openId){
this.openId = openId;
}
/**
* @param api
* @return
*/
@Override
public boolean test(Weixin api) {
return true;
}
/**
* @param api
* @param values
*/
@Override
public void setConnectionValues(Weixin api, ConnectionValues values) {
WeixinUserInfo profile = api.getUserInfo(openId);
values.setProviderUserId(profile.getOpenid());
values.setDisplayName(profile.getNickname());
values.setImageUrl(profile.getHeadimgurl());
}
/**
* @param api
* @return
*/
@Override
public UserProfile fetchUserProfile(Weixin api) {
return null;
}
/**
* @param api
* @param message
*/
@Override
public void updateStatus(Weixin api, String message) {
//do nothing
}
}
- 微信的OAuth2流程处理器的提供器
package com.zcw.security.core.social.weixin.connect;
import com.zcw.security.core.social.weixin.api.Weixin;
import com.zcw.security.core.social.weixin.api.WeixinImpl;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;
/**
* @ClassName : WeixinServiceProvider
* @Description : 微信的OAuth2流程处理器的提供器,供spring social的connect体系调用
* @Author : Zhaocunwei
* @Date: 2020-06-29 14:28
*/
public class WeixinServiceProvider extends AbstractOAuth2ServiceProvider<Weixin> {
/**
* 微信获取授权码的url
*/
private static final String URL_AUTHORIZE = "https://open.weixin.qq.com/connect/qrconnect";
/**
* 微信获取accessToken的url
*/
private static final String URL_ACCESS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token";
/**
* @param appId
* @param appSecret
*/
public WeixinServiceProvider(String appId, String appSecret) {
super(new WeixinOAuth2Template(appId, appSecret,URL_AUTHORIZE,URL_ACCESS_TOKEN));
}
/* (non-Javadoc)
* @see org.springframework.social.oauth2.AbstractOAuth2ServiceProvider#getApi(java.lang.String)
*/
@Override
public Weixin getApi(String accessToken) {
return new WeixinImpl(accessToken);
}
}
- 微信连接工厂
package com.zcw.security.core.social.weixin.connect;
import com.zcw.security.core.social.weixin.api.Weixin;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.support.OAuth2Connection;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2ServiceProvider;
/**
* @ClassName : WeixinConnectionFactory
* @Description : 微信连接工厂
* @Author : Zhaocunwei
* @Date: 2020-06-29 14:29
*/
public class WeixinConnectionFactory extends OAuth2ConnectionFactory<Weixin> {
/**
* @param appId
* @param appSecret
*/
public WeixinConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new WeixinServiceProvider(appId, appSecret), new WeixinAdapter());
}
/**
* 由于微信的openId是和accessToken一起返回的,所以在这里直接根据accessToken设置providerUserId即可,不用像QQ那样通过QQAdapter来获取
*/
@Override
protected String extractProviderUserId(AccessGrant accessGrant) {
if(accessGrant instanceof WeixinAccessGrant) {
return ((WeixinAccessGrant)accessGrant).getOpenId();
}
return null;
}
/* (non-Javadoc)
* @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.oauth2.AccessGrant)
*/
@Override
public Connection<Weixin> createConnection(AccessGrant accessGrant) {
return new OAuth2Connection<Weixin>(getProviderId(), extractProviderUserId(accessGrant), accessGrant.getAccessToken(),
accessGrant.getRefreshToken(), accessGrant.getExpireTime(), getOAuth2ServiceProvider(), getApiAdapter(extractProviderUserId(accessGrant)));
}
/* (non-Javadoc)
* @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.connect.ConnectionData)
*/
@Override
public Connection<Weixin> createConnection(ConnectionData data) {
return new OAuth2Connection<Weixin>(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId()));
}
private ApiAdapter<Weixin> getApiAdapter(String providerUserId) {
return new WeixinAdapter(providerUserId);
}
private OAuth2ServiceProvider<Weixin> getOAuth2ServiceProvider() {
return (OAuth2ServiceProvider<Weixin>) getServiceProvider();
}
}
- 创建微信配置文件
package com.zcw.security.core.social.weixin.config;
import com.zcw.security.core.properties.MySecurityProperties;
import com.zcw.security.core.properties.WeixinProperties;
import com.zcw.security.core.social.view.ZcwConnectView;
import com.zcw.security.core.social.weixin.connect.WeixinConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.social.connect.ConnectionFactory;
import org.springframework.web.servlet.View;
/**
* @ClassName : WeixinAutoConfiguration
* @Description : 微信登录配置
* @Author : Zhaocunwei
* @Date: 2020-06-29 14:45
*/
@Configuration
@ConditionalOnProperty(prefix = "zcw.security.social.weixin", name = "app-id")
public class WeixinAutoConfiguration extends SocialAutoConfigurerAdapter {
@Autowired
private MySecurityProperties securityProperties;
/*
* (non-Javadoc)
*
* @see
* org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter
* #createConnectionFactory()
*/
@Override
protected ConnectionFactory<?> createConnectionFactory() {
WeixinProperties weixinConfig = securityProperties.getSocialProperties().getWeixinProperties();
return new WeixinConnectionFactory(weixinConfig.getProviderId(), weixinConfig.getAppId(),
weixinConfig.getAppSecret());
}
@Bean({"connect/weixinConnect", "connect/weixinConnected"})
@ConditionalOnMissingBean(name = "weixinConnectedView")
public View weixinConnectedView() {
return new ZcwConnectView();
}
}
- 创建绑定结果视图
package com.zcw.security.core.social.view;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.view.AbstractView;
/**
* @ClassName : ZcwConnectView
* @Description : 绑定结果视图
* @Author : Zhaocunwei
* @Date: 2020-06-29 14:48
*/
public class ZcwConnectView extends AbstractView{
/*
* (non-Javadoc)
*
* @see
* org.springframework.web.servlet.view.AbstractView#renderMergedOutputModel
* (java.util.Map, javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.setContentType("text/html;charset=UTF-8");
if (model.get("connections") == null) {
response.getWriter().write("<h3>解绑成功</h3>");
} else {
response.getWriter().write("<h3>绑定成功</h3>");
}
}
}
五、社交账号解绑
- 社交账号绑定状态视图
package com.zcw.security.core.social.view;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.Connection;
import org.springframework.web.servlet.view.AbstractView;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.
/**
* @ClassName : ZcwConnectionStatusView
* @Description : 社交账号绑定状态视图
* @Author : Zhaocunwei
* @Date: 2020-06-29 15:20
*/
@Component("connect/status")
public class ZcwConnectionStatusView extends AbstractView {
@Autowired
private ObjectMapper objectMapper;
/* (non-Javadoc)
* @see org.springframework.web.servlet.view.AbstractView#renderMergedOutputModel(java.util.Map, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@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));
}
}
- 绑定结果视图
package com.zcw.security.core.social.view;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.view.AbstractView;
/**
* @ClassName : ZcwConnectView
* @Description : 绑定结果视图
* @Author : Zhaocunwei
* @Date: 2020-06-29 14:48
*/
public class ZcwConnectView extends AbstractView{
/*
* (non-Javadoc)
*
* @see
* org.springframework.web.servlet.view.AbstractView#renderMergedOutputModel
* (java.util.Map, javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.setContentType("text/html;charset=UTF-8");
if (model.get("connections") == null) {
response.getWriter().write("<h3>解绑成功</h3>");
} else {
response.getWriter().write("<h3>绑定成功</h3>");
}
}
}
六、单机Session管理
- session 超时处理
- session失效
- 创建常量
package com.zcw.security.core.properties;
/**
* @ClassName : SecurityConstants
* @Description : 常量配置
* @Author : Zhaocunwei
* @Date: 2020-06-29 16:06
*/
public class SecurityConstants {
/**
* 默认的处理验证码的url前缀
*/
String DEFAULT_VALIDATE_CODE_URL_PREFIX = "/code";
/**
* 当请求需要身份认证时,默认跳转的url
*
* @see SecurityController
*/
String DEFAULT_UNAUTHENTICATION_URL = "/authentication/require";
/**
* 默认的用户名密码登录请求处理url
*/
String DEFAULT_SIGN_IN_PROCESSING_URL_FORM = "/authentication/form";
/**
* 默认的手机验证码登录请求处理url
*/
String DEFAULT_SIGN_IN_PROCESSING_URL_MOBILE = "/authentication/mobile";
/**
* 默认的OPENID登录请求处理url
*/
String DEFAULT_SIGN_IN_PROCESSING_URL_OPENID = "/authentication/openid";
/**
* 默认登录页面
*
* @see SecurityController
*/
String DEFAULT_SIGN_IN_PAGE_URL = "/zcw-signIn.html";
/**
* 验证图片验证码时,http请求中默认的携带图片验证码信息的参数的名称
*/
String DEFAULT_PARAMETER_NAME_CODE_IMAGE = "imageCode";
/**
* 验证短信验证码时,http请求中默认的携带短信验证码信息的参数的名称
*/
String DEFAULT_PARAMETER_NAME_CODE_SMS = "smsCode";
/**
* 发送短信验证码 或 验证短信验证码时,传递手机号的参数的名称
*/
String DEFAULT_PARAMETER_NAME_MOBILE = "mobile";
/**
* openid参数名
*/
String DEFAULT_PARAMETER_NAME_OPENID = "openId";
/**
* providerId参数名
*/
String DEFAULT_PARAMETER_NAME_PROVIDERID = "providerId";
/**
* session失效默认的跳转地址
*/
String DEFAULT_SESSION_INVALID_URL = "/zcw-session-invalid.html";
/**
* 获取第三方用户信息的url
*/
String DEFAULT_SOCIAL_USER_INFO_URL = "/social/user";
}
- session管理相关配置项
package com.zcw.security.core.properties;
/**
* @ClassName : SessionProperties
* @Description : session管理相关配置项
* @Author : Zhaocunwei
* @Date: 2020-06-29 16:14
*/
public class SessionProperties {
/**
* 同一个用户在系统中的最大session数,默认1
*/
private int maximumSessions = 1;
/**
* 达到最大session时是否阻止新的登录请求,默认为false,不阻止,新的登录会将老的登录失效掉
*/
private boolean maxSessionsPreventsLogin;
/**
* session失效时跳转的地址
*/
private String sessionInvalidUrl = SecurityConstants.DEFAULT_SESSION_INVALID_URL;
public int getMaximumSessions() {
return maximumSessions;
}
public void setMaximumSessions(int maximumSessions) {
this.maximumSessions = maximumSessions;
}
public boolean isMaxSessionsPreventsLogin() {
return maxSessionsPreventsLogin;
}
public void setMaxSessionsPreventsLogin(boolean maxSessionsPreventsLogin) {
this.maxSessionsPreventsLogin = maxSessionsPreventsLogin;
}
public String getSessionInvalidUrl() {
return sessionInvalidUrl;
}
public void setSessionInvalidUrl(String sessionInvalidUrl) {
this.sessionInvalidUrl = sessionInvalidUrl;
}
}
- session并发控制(并发登录导致session失效时,默认的处理策略)
package com.zcw.security.browser.session;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zcw.security.browser.support.SimpleResponse;
import com.zcw.security.core.properties.MySecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ClassName : AbstractSessionStrategy
* @Description : 抽象的session失效处理器
* @Author : Zhaocunwei
* @Date: 2020-06-29 16:10
*/
@Slf4j
public class AbstractSessionStrategy {
/**
* 跳转的url
*/
private String destinationUrl;
/**
* 系统配置信息
*/
private MySecurityProperties mysecurityPropertie;
/**
* 重定向策略
*/
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/**
* 跳转前是否创建新的session
*/
private boolean createNewSession = true;
private ObjectMapper objectMapper = new ObjectMapper();
/**
* @param invalidSessionUrl
* @param invalidSessionHtmlUrl
*/
public AbstractSessionStrategy(MySecurityProperties mysecurityPropertie) {
String invalidSessionUrl = mysecurityPropertie.getBrowserProperties().getSessionProperties().getSessionInvalidUrl();
Assert.isTrue(UrlUtils.isValidRedirectUrl(invalidSessionUrl), "url must start with '/' or with 'http(s)'");
Assert.isTrue(StringUtils.endsWithIgnoreCase(invalidSessionUrl, ".html"), "url must end with '.html'");
this.destinationUrl = invalidSessionUrl;
this.mysecurityPropertie = mysecurityPropertie;
}
/*
* (non-Javadoc)
*
* @see org.springframework.security.web.session.InvalidSessionStrategy#
* onInvalidSessionDetected(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
protected void onSessionInvalid(HttpServletRequest request, HttpServletResponse response) throws IOException {
log.info("session失效");
if (createNewSession) {
request.getSession();
}
String sourceUrl = request.getRequestURI();
String targetUrl;
if (StringUtils.endsWithIgnoreCase(sourceUrl, ".html")) {
if(StringUtils.equals(sourceUrl, mysecurityPropertie.getBrowserProperties().getSignInPage())
|| StringUtils.equals(sourceUrl, mysecurityPropertie.getBrowserProperties().getSignOutUrl())){
targetUrl = sourceUrl;
}else{
targetUrl = destinationUrl;
}
log.info("跳转到:"+targetUrl);
redirectStrategy.sendRedirect(request, response, targetUrl);
} else {
Object result = buildResponseContent(request);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(result));
}
}
/**
* @param request
* @return
*/
protected Object buildResponseContent(HttpServletRequest request) {
String message = "session已失效";
if (isConcurrency()) {
message = message + ",有可能是并发登录导致的";
}
return new SimpleResponse(message);
}
/**
* session失效是否是并发导致的
*
* @return
*/
protected boolean isConcurrency() {
return false;
}
/**
* Determines whether a new session should be created before redirecting (to
* avoid possible looping issues where the same session ID is sent with the
* redirected request). Alternatively, ensure that the configured URL does
* not pass through the {@code SessionManagementFilter}.
*
* @param createNewSession
* defaults to {@code true}.
*/
public void setCreateNewSession(boolean createNewSession) {
this.createNewSession = createNewSession;
}
}
package com.zcw.security.browser.session;
import java.io.IOException;
import javax.servlet.ServletException;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
/**
* @ClassName : ZcwExpiredSessionStrategy
* @Description : 并发登录导致session失效时,默认的处理策略
* @Author : Zhaocunwei
* @Date: 2020-06-29 16:09
*/
public class ZcwExpiredSessionStrategy extends AbstractSessionStrategy implements SessionInformationExpiredStrategy {
public ZcwExpiredSessionStrategy(SecurityProperties securityPropertie) {
super(securityPropertie);
}
/* (non-Javadoc)
* @see org.springframework.security.web.session.SessionInformationExpiredStrategy#onExpiredSessionDetected(org.springframework.security.web.session.SessionInformationExpiredEvent)
*/
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
onSessionInvalid(event.getRequest(), event.getResponse());
}
/* (non-Javadoc)
* @see com.imooc.security.browser.session.AbstractSessionStrategy#isConcurrency()
*/
@Override
protected boolean isConcurrency() {
return true;
}
}
package com.zcw.security.core.properties;
import lombok.Data;
/**
* @ClassName : BrowserProperties
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-19 13:55
*/
public class BrowserProperties {
private String loginPage = "/zcw-sigIn.html";
private LoginType loginType = LoginType.JSON;
private SessionProperties sessionProperties = new SessionProperties();
private String signInPage = SecurityConstants.DEFAULT_SIGN_IN_PAGE_URL;
/**
* 退出成功时跳转的url,如果配置了,则跳到指定的url,如果没配置,则返回json数据。
*/
private String signOutUrl;
/**
* 配置注册URL地址
*/
private String singUpUrl;
//配置token过期的秒数
private int remeberMeSeconds=3600;
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
public LoginType getLoginType() {
return loginType;
}
public void setLoginType(LoginType loginType) {
this.loginType = loginType;
}
public int getRemeberMeSeconds() {
return remeberMeSeconds;
}
public void setRemeberMeSeconds(int remeberMeSeconds) {
this.remeberMeSeconds = remeberMeSeconds;
}
public String getSingUpUrl() {
return singUpUrl;
}
public void setSingUpUrl(String singUpUrl) {
this.singUpUrl = singUpUrl;
}
public SessionProperties getSessionProperties() {
return sessionProperties;
}
public void setSessionProperties(SessionProperties sessionProperties) {
this.sessionProperties = sessionProperties;
}
public String getSignInPage() {
return signInPage;
}
public void setSignInPage(String signInPage) {
this.signInPage = signInPage;
}
public String getSignOutUrl() {
return signOutUrl;
}
public void setSignOutUrl(String signOutUrl) {
this.signOutUrl = signOutUrl;
}
}
- 修改配置文件
package com.zcw.security.browser;
import com.zcw.security.core.properties.MySecurityProperties;
import com.zcw.security.core.validate.code.SmsCodeFilter;
import com.zcw.security.core.validate.code.ValidateCodeFilter;
import com.zcw.security.core.validate.code.sms.SmsCodeAuthenticationSecurityConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.social.security.SpringSocialConfigurer;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import javax.xml.ws.soap.Addressing;
/**
* @ClassName : BrowserSecurityConfig
* @Description :适配器类
* @Author : Zhaocunwei
* @Date: 2020-06-18 17:43
*/
@Component
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationFailureHandler zcwAuthenticationFailureHandler;
@Autowired
private MySecurityProperties mySecurityProperties;
@Autowired
private AuthenticationSuccessHandler zcwAuthenticationSuccessHandler;
@Autowired
private DataSource dataSource;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Autowired
private SpringSocialConfigurer zcwSpcialSecurityConfig;
@Autowired
private InvalidSessionStrategy invalidSessionStrategy;
@Autowired
private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//启动时创建表,也可以直接进入源代码执行脚本,建议执行脚本,这个地方不要配置
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(zcwAuthenticationFailureHandler);
validateCodeFilter.setMySecurityProperties(mySecurityProperties);
//短信验证码过滤器配置
SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
smsCodeFilter.setAuthenticationFailureHandler(zcwAuthenticationFailureHandler);
smsCodeFilter.setMySecurityProperties(mySecurityProperties);
smsCodeFilter.afterPropertiesSet();
//调用初始化方法
validateCodeFilter.afterPropertiesSet();
//表单登录
http.addFilterBefore(smsCodeFilter,UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(zcwAuthenticationSuccessHandler)
.failureHandler(zcwAuthenticationFailureHandler)
//记住我的配置
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
//配置token 过期的秒数
.tokenValiditySeconds(mySecurityProperties
.getBrowserProperties()
.getRemeberMeSeconds())
.userDetailsService(userDetailsService)
.and()
.sessionManagement()
.invalidSessionStrategy(invalidSessionStrategy)
.maximumSessions(mySecurityProperties.getBrowserProperties()
.getSessionProperties().getMaximumSessions())
.maxSessionsPreventsLogin(mySecurityProperties
.getBrowserProperties()
.getSessionProperties()
.isMaxSessionsPreventsLogin())
.expiredSessionStrategy(sessionInformationExpiredStrategy)
.and()
//授权
.authorizeRequests()
//授权匹配器
.antMatchers("/authentication/require",
mySecurityProperties.getBrowserProperties().getLoginPage(),
mySecurityProperties.getBrowserProperties().getSingUpUrl(),
"/code/image",
"/user/regist").permitAll()
.anyRequest()
.authenticated()
.and()
.apply(zcwSpcialSecurityConfig)
.and()
.csrf().disable()//跨站攻击被禁用
.apply(smsCodeAuthenticationSecurityConfig);
}
}
七、集群session
一般需要配置redis,整合集群session
目前spring-session 框架处理我们这个集群session
八、退出登录
package com.zcw.security.core.authentication;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
/**
* @ClassName : AuthorizeConfigManager
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-29 16:48
*/
public interface AuthorizeConfigManager {
/**
* @param config
*/
void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config);
}
package com.zcw.security.browser;
import com.zcw.security.core.authentication.AuthorizeConfigManager;
import com.zcw.security.core.properties.MySecurityProperties;
import com.zcw.security.core.validate.code.SmsCodeFilter;
import com.zcw.security.core.validate.code.ValidateCodeFilter;
import com.zcw.security.core.validate.code.sms.SmsCodeAuthenticationSecurityConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.social.security.SpringSocialConfigurer;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import javax.xml.ws.soap.Addressing;
/**
* @ClassName : BrowserSecurityConfig
* @Description :适配器类
* @Author : Zhaocunwei
* @Date: 2020-06-18 17:43
*/
@Component
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationFailureHandler zcwAuthenticationFailureHandler;
@Autowired
private MySecurityProperties mySecurityProperties;
@Autowired
private AuthenticationSuccessHandler zcwAuthenticationSuccessHandler;
@Autowired
private DataSource dataSource;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Autowired
private SpringSocialConfigurer zcwSpcialSecurityConfig;
@Autowired
private InvalidSessionStrategy invalidSessionStrategy;
@Autowired
private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
@Autowired
private AuthorizeConfigManager authorizeConfigManager;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//启动时创建表,也可以直接进入源代码执行脚本,建议执行脚本,这个地方不要配置
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(zcwAuthenticationFailureHandler);
validateCodeFilter.setMySecurityProperties(mySecurityProperties);
//短信验证码过滤器配置
SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
smsCodeFilter.setAuthenticationFailureHandler(zcwAuthenticationFailureHandler);
smsCodeFilter.setMySecurityProperties(mySecurityProperties);
smsCodeFilter.afterPropertiesSet();
//调用初始化方法
validateCodeFilter.afterPropertiesSet();
//表单登录
http.addFilterBefore(smsCodeFilter,UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(zcwAuthenticationSuccessHandler)
.failureHandler(zcwAuthenticationFailureHandler)
//记住我的配置
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
//配置token 过期的秒数
.tokenValiditySeconds(mySecurityProperties
.getBrowserProperties()
.getRemeberMeSeconds())
.userDetailsService(userDetailsService)
.and()
.sessionManagement()
.invalidSessionStrategy(invalidSessionStrategy)
.maximumSessions(mySecurityProperties.getBrowserProperties()
.getSessionProperties().getMaximumSessions())
.maxSessionsPreventsLogin(mySecurityProperties
.getBrowserProperties()
.getSessionProperties()
.isMaxSessionsPreventsLogin())
.expiredSessionStrategy(sessionInformationExpiredStrategy)
.and()
//授权匹配器
.antMatchers("/authentication/require",
mySecurityProperties.getBrowserProperties().getLoginPage(),
mySecurityProperties.getBrowserProperties().getSingUpUrl(),
"/code/image",
"/user/regist").permitAll()
.anyRequest()
.authenticated()
.and()
.apply(zcwSpcialSecurityConfig)
.and()
.logout()
.logoutUrl("/signOut")
.logoutSuccessHandler(logoutSuccessHandler)
.deleteCookies("JSESSIONID")
.and()
.csrf().disable()//跨站攻击被禁用
.apply(smsCodeAuthenticationSecurityConfig);
//授权
authorizeConfigManager.config(http.authorizeRequests());
}
}
package com.zcw.security.browser.logout;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zcw.security.browser.support.SimpleResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ClassName : ZcwLogoutSuccessHandler
* @Description :默认的退出成功处理器,如果设置了zcw.security.browser.signOutUrl,则跳到配置的地址上,
* * 如果没配置,则返回json格式的响应。
* @Author : Zhaocunwei
* @Date: 2020-06-29 16:54
*/
@Slf4j
public class ZcwLogoutSuccessHandler implements LogoutSuccessHandler {
public ZcwLogoutSuccessHandler(String signOutSuccessUrl) {
this.signOutSuccessUrl = signOutSuccessUrl;
}
private String signOutSuccessUrl;
private ObjectMapper objectMapper = new ObjectMapper();
/*
* (non-Javadoc)
*
* @see org.springframework.security.web.authentication.logout.
* LogoutSuccessHandler#onLogoutSuccess(javax.servlet.http.
* HttpServletRequest, javax.servlet.http.HttpServletResponse,
* org.springframework.security.core.Authentication)
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
log.info("退出成功");
if (StringUtils.isBlank(signOutSuccessUrl)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse("退出成功")));
} else {
response.sendRedirect(signOutSuccessUrl);
}
}
}