OAuth協(xié)議
image.png
OAuth協(xié)議中的授權(quán)模式
- 授權(quán)碼模式(authorization code)
- 密碼模式(resource owner password credentials)
- 客戶端模式(client credentials)
- 簡(jiǎn)化模式(implicit)
授權(quán)碼模式
image.png
spring social基本原理
image.png
image.png
QQ登陸
返回User封裝好到對(duì)象
public class QQUserInfo {
/**
*QQ
*/
private String openId;
private String constellation;
private String is_lost;
/**
* 返回碼
*/
private String ret;
/**
* 如果ret<0,會(huì)有相應(yīng)的錯(cuò)誤信息提示粥庄,返回?cái)?shù)據(jù)全部用UTF-8編碼丧失。
*/
private String msg;
/**
* 用戶在QQ空間的昵稱。
*/
private String nickname;
/**
* 出生年
*/
private String year;
/**
* 性別默認(rèn)男
*/
String gender;
/**
* 省
*/
String province;
/**
* 市
*/
private String city;
private String figureurl_type;
/**
* 大小為30×30像素的QQ空間頭像URL惜互。
*/
private String figureurl;
/**
* 大小為50×50像素的QQ空間頭像URL布讹。
*/
private String figureurl_1;
/**
* 大小為100×100像素的QQ空間頭像URL琳拭。
*/
private String figureurl_2;
private String figureurl_qq;
/**
* 大小為40×40像素的QQ頭像URL。
*/
private String figureurl_qq_1;
/**
* 大小為100×100像素的QQ頭像URL描验。需要注意白嘁,不是所有的用戶都擁有QQ的100×100的頭像,但40×40像素則是一定會(huì)有膘流。
*/
private String figureurl_qq_2;
/**
* 標(biāo)識(shí)用戶是否為黃鉆用戶(0:不是絮缅;1:是)。
*/
private String is_yellow_vip;
/**
* 標(biāo)識(shí)用戶是否為黃鉆用戶(0:不是呼股;1:是)
*/
private String vip;
/**
* 黃鉆等級(jí)
*/
private String yellow_vip_level;
/**
* 黃鉆等級(jí)
*/
private String level;
/**
* 標(biāo)識(shí)是否為年費(fèi)黃鉆用戶(0:不是耕魄; 1:是)
*/
private String is_yellow_year_vip;
public String getOpenId() {
return openId;
}
public void setOpenId(String openId) {
this.openId = openId;
}
public String getIs_lost() {
return is_lost;
}
public void setIs_lost(String is_lost) {
this.is_lost = is_lost;
}
public String getRet() {
return ret;
}
public void setRet(String ret) {
this.ret = ret;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getYear() {
return year;
}
public void setYear(String year) {
this.year = year;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getFigureurl() {
return figureurl;
}
public void setFigureurl(String figureurl) {
this.figureurl = figureurl;
}
public String getFigureurl_1() {
return figureurl_1;
}
public void setFigureurl_1(String figureurl_1) {
this.figureurl_1 = figureurl_1;
}
public String getFigureurl_2() {
return figureurl_2;
}
public void setFigureurl_2(String figureurl_2) {
this.figureurl_2 = figureurl_2;
}
public String getFigureurl_qq() {
return figureurl_qq;
}
public void setFigureurl_qq(String figureurl_qq) {
this.figureurl_qq = figureurl_qq;
}
public String getFigureurl_qq_1() {
return figureurl_qq_1;
}
public void setFigureurl_qq_1(String figureurl_qq_1) {
this.figureurl_qq_1 = figureurl_qq_1;
}
public String getFigureurl_qq_2() {
return figureurl_qq_2;
}
public void setFigureurl_qq_2(String figureurl_qq_2) {
this.figureurl_qq_2 = figureurl_qq_2;
}
public String getIs_yellow_vip() {
return is_yellow_vip;
}
public void setIs_yellow_vip(String is_yellow_vip) {
this.is_yellow_vip = is_yellow_vip;
}
public String getVip() {
return vip;
}
public void setVip(String vip) {
this.vip = vip;
}
public String getYellow_vip_level() {
return yellow_vip_level;
}
public void setYellow_vip_level(String yellow_vip_level) {
this.yellow_vip_level = yellow_vip_level;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
public String getIs_yellow_year_vip() {
return is_yellow_year_vip;
}
public void setIs_yellow_year_vip(String is_yellow_year_vip) {
this.is_yellow_year_vip = is_yellow_year_vip;
}
public String getConstellation() {
return constellation;
}
public void setConstellation(String constellation) {
this.constellation = constellation;
}
public String getFigureurl_type() {
return figureurl_type;
}
public void setFigureurl_type(String figureurl_type) {
this.figureurl_type = figureurl_type;
}
public interface QQ {
/**
* 獲取用戶信息
* @return
*/
public QQUserInfo getUserInfo();
}
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
private Logger logger= LoggerFactory.getLogger(getClass());
/**
* 獲取openId
*/
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";
//QQ互聯(lián)應(yīng)用id
private String appId;
//QQ號(hào)碼
private String openId;
private ObjectMapper objectMapper=new ObjectMapper();
public QQImpl(String accessToken,String appId){
/**
* TokenStrategy.ACCESS_TOKEN_PARAMETER 表示將accessToken放到傳入?yún)?shù)里面
*/
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
this.appId=appId;
String url=String.format(URL_GET_OPENID,accessToken);
String result=getRestTemplate().getForObject(url,String.class);
logger.info(result);
this.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
logger.info("openid"+openId);
}
/**
* 獲取用戶信息
* @return
* @throws IOException
*/
@Override
public QQUserInfo getUserInfo() {
String url=String.format(URL_GET_USERINFO,appId,openId);
String result=getRestTemplate().getForObject(url,String.class);
logger.info(result);
QQUserInfo qqUserInfo=null;
try {
qqUserInfo=objectMapper.readValue(result,QQUserInfo.class);
qqUserInfo.setOpenId(openId);
return qqUserInfo;
} catch (IOException e) {
throw new RuntimeException("獲取用戶信息失敗");
}
}
}
@Configuration
//如果沒配置appId與appSecret擇配置類不生效
@ConditionalOnProperty(prefix = "guosh.security.social.qq",name = {"appId","appSecret"})
public class QQAutoConfig extends SocialAutoConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Override
protected ConnectionFactory<?> createConnectionFactory() {
QQProperties qqconfig= securityProperties.getSocial().getQq();
//QQ連接工廠
return new QQConnectionFactory(qqconfig.getProviderId(),qqconfig.getAppId(),qqconfig.getAppSecret());
}
}
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
public QQConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new QQServerProvider(appId,appSecret), new QQAdapter());
}
}
public class QQServerProvider extends AbstractOAuth2ServiceProvider<QQ> {
private String appId;
//導(dǎo)向認(rèn)證服務(wù)器
private static final String URL_AUTHORIZE="https://graph.qq.com/oauth2.0/authorize";
//申請(qǐng)令牌
private static final String URL_ACCESS_TOKEN="https://graph.qq.com/oauth2.0/token";
public QQServerProvider(String appId,String appSecret) {
super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
this.appId = appId;
}
@Override
public QQ getApi(String accessToken) {
return new QQImpl(accessToken,appId);
}
}
public class QQOAuth2Template extends OAuth2Template {
private Logger logger= LoggerFactory.getLogger(getClass());
public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
setUseParametersForClientAuthentication(true);
}
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate= super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
/**
*處理返回的QQ token格式
* @param accessTokenUrl
* @param parameters
* @return
*/
@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
String responseStr= getRestTemplate().postForObject(accessTokenUrl,parameters,String.class);
logger.info("獲取accessToken響應(yīng)"+responseStr);
//分割字符串
String[]items=StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr,"&");
//token
String accessToken = StringUtils.substringAfterLast(items[0], "=");
Long expiresIn = new Long(StringUtils.substringAfterLast(items[1], "="));
//刷新令牌
String refreshToken = StringUtils.substringAfterLast(items[2], "=");
return new AccessGrant(accessToken,null,refreshToken,expiresIn);
}
}
public class QQAdapter implements ApiAdapter<QQ> {
//測(cè)試Api是否可用
@Override
public boolean test(QQ qq) {
return true;
}
@Override
public void setConnectionValues(QQ api, ConnectionValues values) {
QQUserInfo userInfo=api.getUserInfo();
//名稱
values.setDisplayName(userInfo.getNickname());
//頭像
values.setImageUrl(userInfo.getFigureurl_qq());
//主頁(yè)
values.setProfileUrl(null);
//openId
values.setProviderUserId(userInfo.getOpenId());
}
@Override
public UserProfile fetchUserProfile(QQ qq) {
return null;
}
@Override
public void updateStatus(QQ qq, String s) {
//發(fā)送消息更新動(dòng)態(tài)
}
}
public class MySpringSocialConfigurer extends SpringSocialConfigurer {
private String filtertProcessesUrl;
public MySpringSocialConfigurer(String filtertProcessesUrl) {
this.filtertProcessesUrl = filtertProcessesUrl;
}
/**
* 第三方認(rèn)證回調(diào)路徑
* @param object
* @param <T>
* @return
*/
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter= (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filtertProcessesUrl);
return (T) filter;
}
}
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private SecurityProperties securityProperties;
@Autowired(required = false)
private ConnectionSignUp connectionSignUp;
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
//把互聯(lián)數(shù)據(jù)存儲(chǔ)到表
JdbcUsersConnectionRepository repository= new JdbcUsersConnectionRepository(dataSource,connectionFactoryLocator, Encryptors.noOpText());
repository.setTablePrefix("sys_");
//把qq讀區(qū)出來的數(shù)據(jù)自動(dòng)注冊(cè)
if(connectionSignUp!=null){
repository.setConnectionSignUp(connectionSignUp);
}
return repository;
}
@Bean
public SpringSocialConfigurer sociaSecurityConfig(){
String filtertProcessesUrl=securityProperties.getSocial().getFiltertProcessesUrl();
MySpringSocialConfigurer configurer=new MySpringSocialConfigurer(filtertProcessesUrl);
configurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());//指定到注冊(cè)頁(yè)面
return configurer;
}
//免注冊(cè)
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator){
return new ProviderSignInUtils(connectionFactoryLocator,getUsersConnectionRepository(connectionFactoryLocator));
}
}
微信登陸
public class WeixinUserInfo {
/**
* 普通用戶的標(biāo)識(shí),對(duì)當(dāng)前開發(fā)者帳號(hào)唯一
*/
private String openid;
/**
* 普通用戶昵稱
*/
private String nickname;
/**
* 語(yǔ)言
*/
private String language;
/**
* 普通用戶性別彭谁,1為男性吸奴,2為女性
*/
private String sex;
/**
* 普通用戶個(gè)人資料填寫的省份
*/
private String province;
/**
* 普通用戶個(gè)人資料填寫的城市
*/
private String city;
/**
* 國(guó)家,如中國(guó)為CN
*/
private String country;
/**
* 用戶頭像缠局,最后一個(gè)數(shù)值代表正方形頭像大性虬隆(有0、46、64、96抬旺、132數(shù)值可選象对,0代表640*640正方形頭像),用戶沒有頭像時(shí)該項(xiàng)為空
*/
private String headimgurl;
/**
* 用戶特權(quán)信息,json數(shù)組,如微信沃卡用戶為(chinaunicom)
*/
private String[] privilege;
/**
* 用戶統(tǒng)一標(biāo)識(shí)。針對(duì)一個(gè)微信開放平臺(tái)帳號(hào)下的應(yīng)用馆匿,同一用戶的unionid是唯一的。
*/
private String unionid;
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getHeadimgurl() {
return headimgurl;
}
public void setHeadimgurl(String headimgurl) {
this.headimgurl = headimgurl;
}
public String[] getPrivilege() {
return privilege;
}
public void setPrivilege(String[] privilege) {
this.privilege = privilege;
}
public String getUnionid() {
return unionid;
}
public void setUnionid(String unionid) {
this.unionid = unionid;
}
}
public interface Weixin {
/**
* 獲取用戶信息
* @return
*/
public WeixinUserInfo getUserInfo(String openId);
}
public class WeixinImpl extends AbstractOAuth2ApiBinding implements Weixin{
private Logger logger= LoggerFactory.getLogger(getClass());
/**
*
*/
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);
}
/**
* 默認(rèn)注冊(cè)的StringHttpMessageConverter字符集為ISO-8859-1燥滑,而微信返回的是UTF-8的渐北,所以覆蓋了原來的方法。
*/
protected List<HttpMessageConverter<?>> getMessageConverters() {
List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters();
messageConverters.remove(0);
messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return messageConverters;
}
/**
* 用戶信息獲取
* @param openId
* @return
*/
@Override
public WeixinUserInfo getUserInfo(String openId) {
String url = URL_GET_USER_INFO + openId;
String response = getRestTemplate().getForObject(url, String.class);
logger.info(response);
if(StringUtils.contains(response, "errcode")) {
return null;
}
WeixinUserInfo profile = null;
try {
profile = objectMapper.readValue(response, WeixinUserInfo.class);
} catch (Exception e) {
e.printStackTrace();
}
return profile;
}
}
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;
}
}
/**
* 微信 api適配器铭拧,將微信 api的數(shù)據(jù)模型轉(zhuǎn)為spring social的標(biāo)準(zhǔn)模型赃蛛。
*
*
* @author guosh
*
*/
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
}
}
/**
* 微信連接工廠
*
* @author guosh
*
*/
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一起返回的,所以在這里直接根據(jù)accessToken設(shè)置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)
*/
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)
*/
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();
}
}
/**
*
* 完成微信的OAuth2認(rèn)證流程的模板類呕臂。國(guó)內(nèi)廠商實(shí)現(xiàn)的OAuth2每個(gè)都不同, spring默認(rèn)提供的OAuth2Template適應(yīng)不了,只能針對(duì)每個(gè)廠商自己微調(diào)肪跋。
*
* @author guosh
*
*/
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);
}
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, 請(qǐng)求URL: "+accessTokenRequestUrl.toString());
String response = getRestTemplate().getForObject(accessTokenRequestUrl.toString(), String.class);
logger.info("獲取access_token, 響應(yīng)內(nèi)容: "+response);
Map<String, Object> result = null;
try {
result = new ObjectMapper().readValue(response, Map.class);
} catch (Exception e) {
e.printStackTrace();
}
//返回錯(cuò)誤碼時(shí)直接返回空
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;
}
/**
* 構(gòu)建獲取授權(quán)碼的請(qǐng)求歧蒋。也就是引導(dǎo)用戶跳轉(zhuǎn)到微信的地址。
*/
public String buildAuthenticateUrl(OAuth2Parameters parameters) {
String url = super.buildAuthenticateUrl(parameters);
url = url + "&appid="+clientId+"&scope=snsapi_login";
return url;
}
public String buildAuthorizeUrl(OAuth2Parameters parameters) {
return buildAuthenticateUrl(parameters);
}
/**
* 微信返回的contentType是html/text,添加相應(yīng)的HttpMessageConverter來處理谜洽。
*/
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
}
/**
*
* 微信的OAuth2流程處理器的提供器萝映,供spring social的connect體系調(diào)用
*
* @author guosh
*
*/
public class WeixinServiceProvider extends AbstractOAuth2ServiceProvider<Weixin> {
/**
* 微信獲取授權(quán)碼的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);
}
}
綁定解綁
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>綁定解綁</title>
</head>
<body>
<h2>標(biāo)準(zhǔn)綁定頁(yè)面</h2>
<!-- 第一個(gè)值固定第二個(gè)是providerId 請(qǐng)求類型換成delete就是解除綁定-->
<form action="/connect/weixin" method="post">
<button type="submit">綁定微信</button>
</form>
</body>
</html>
查詢當(dāng)前賬戶第三方綁定情況視圖
/**
* 返回當(dāng)前賬戶哪些第三方登陸已經(jīng)綁定(綁定與解綁)
* @Author: Guosh
* @Date: 2019-06-03 14:09
*/
@Component("connect/status")
public class ConnectionStatusView extends AbstractView {
@Autowired
private ObjectMapper objectMapper;
@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));
}
}
綁定與解綁
/**
* 綁定成功返回視圖
* @Author: Guosh
* @Date: 2019-06-03 15:49
*/
public class ConnetView 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("connections") == null) {
response.getWriter().write("<h3>解綁成功</h3>");
} else {
response.getWriter().write("<h3>綁定成功</h3>");
}
}
}
@Configuration
@ConditionalOnProperty(prefix = "guosh.security.social.weixin", name = {"appId","appSecret"})
public class WeixinAutoConfiguration extends SocialAutoConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
/*
* (non-Javadoc)
*
* @see
* org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter
* #createConnectionFactory()
*/
@Override
protected ConnectionFactory<?> createConnectionFactory() {
WinxinProperties weixinConfig = securityProperties.getSocial().getWeixin();
return new WeixinConnectionFactory(weixinConfig.getProviderId(), weixinConfig.getAppId(),
weixinConfig.getAppSecret());
}
/**
* 不帶ed是解綁的視圖帶ed是綁定帶視圖
* @return
*/
@Bean({"connect/weixinConnect","connect/weixinConnected"})
@ConditionalOnMissingBean(name = "weixinConnectedView")
public View weixinConnectedView() {
return new ConnetView();
}
}
Session管理
Session超時(shí)處理
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
//自定義配置文件
@Autowired
private SecurityProperties securityProperties;
//登陸相關(guān)配置
@Autowired
private FormAuthenticationConfig formAuthenticationConfig;
//校驗(yàn)驗(yàn)證碼
@Autowired
private ValidateCodeSecurityConfig validateCodeSecurityConfig;
//手機(jī)號(hào)驗(yàn)證碼登陸方式
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
//登陸實(shí)現(xiàn)
@Autowired
private UserDetailsService userDetailsService;
//第三方登陸
@Autowired
private SpringSocialConfigurer sociaSecurityConfig;
//數(shù)據(jù)源
@Autowired
private DataSource dataSource;
//處理session失效
@Autowired
private InvalidSessionStrategy invalidSessionStrategy;
//處理最大登陸數(shù)
@Autowired
private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;
//記住密碼
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//啟動(dòng)自動(dòng)創(chuàng)建persistent_logins表
//tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
formAuthenticationConfig.configure(http);
http
.csrf().disable()//關(guān)閉跨站防護(hù)
.apply(validateCodeSecurityConfig) //校驗(yàn)驗(yàn)證碼
.and()
.apply(smsCodeAuthenticationSecurityConfig)//手機(jī)驗(yàn)證碼登陸
.and()
.apply(sociaSecurityConfig) //第三方登陸
.and()
.authorizeRequests()//下面授權(quán)配置
.antMatchers(
SecurityConstants.DEFAULT_UNAUTHENTICATION_URL, //處理登陸請(qǐng)求
SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_MOBILE, //手機(jī)登陸
securityProperties.getBrowser().getLoginPage(), //登陸頁(yè)面
securityProperties.getBrowser().getSignUpUrl(), //注冊(cè)頁(yè)面
SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*", //驗(yàn)證碼
"/user/regist")//第三方注冊(cè)跟綁定
.permitAll()//login請(qǐng)求除外不需要認(rèn)證
.anyRequest()
.authenticated()//所有請(qǐng)求都需要身份認(rèn)證
.and()
.rememberMe() //記住密碼
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()) //失效時(shí)間
.userDetailsService(userDetailsService)
.and()
.sessionManagement()
.invalidSessionStrategy(invalidSessionStrategy) //session失效后的處理
.maximumSessions(securityProperties.getBrowser().getSession().getMaximumSessions()) //用戶最大登陸數(shù)
.maxSessionsPreventsLogin(securityProperties.getBrowser().getSession().isMaxSessionsPreventsLogin())//是否阻止登陸
.expiredUrl(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//用戶只能登陸一次
.expiredSessionStrategy(sessionInformationExpiredStrategy);//用戶被擠掉后的處理
}
}
/**
* @author guosh
*
*/
public class AbstractSessionStrategy {
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* 跳轉(zhuǎn)的url
*/
private String destinationUrl;
/**
* 重定向策略
*/
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/**
* 跳轉(zhuǎn)前是否創(chuàng)建新的session
*/
private boolean createNewSession = true;
private ObjectMapper objectMapper = new ObjectMapper();
/**
* @param invalidSessionUrl
*/
public AbstractSessionStrategy(String invalidSessionUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(invalidSessionUrl), "url must start with '/' or with 'http(s)'");
this.destinationUrl = invalidSessionUrl;
}
/*
* (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 {
if (createNewSession) {
request.getSession();
}
String sourceUrl = request.getRequestURI();
String targetUrl;
//if (StringUtils.endsWithIgnoreCase(sourceUrl, ".html")) {
targetUrl = destinationUrl;
logger.info("session失效,跳轉(zhuǎn)到"+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 + ",有可能是并發(fā)登錄導(dǎo)致的";
}
return new SimpleResponse(message);
}
/**
* session失效是否是并發(fā)導(dǎo)致的
* @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;
}
}
public class MyInvalidSessionStrategy extends AbstractSessionStrategy implements InvalidSessionStrategy {
/**
* @param invalidSessionUrl
*/
public MyInvalidSessionStrategy(String invalidSessionUrl) {
super(invalidSessionUrl);
}
@Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
onSessionInvalid(request, response);
}
}
public class MyExpiredSessionStrategy extends AbstractSessionStrategy implements SessionInformationExpiredStrategy {
public MyExpiredSessionStrategy(String invalidSessionUrl) {
super(invalidSessionUrl);
}
/* (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;
}
}
session集群處理
配置文件更改
session:
store-type: redis
退出登陸
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
private String signOutSuccessUrl;
private Logger logger= LoggerFactory.getLogger(getClass());
private ObjectMapper objectMapper = new ObjectMapper();
private RedirectStrategy redirectStrategy=new DefaultRedirectStrategy();
public MyLogoutSuccessHandler(String signOutSuccessUrl) {
this.signOutSuccessUrl = signOutSuccessUrl;
}
@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 {
redirectStrategy.sendRedirect(request,response,signOutSuccessUrl);
}
}
}
@Configuration
public class BrowserSecurityBeanConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
@ConditionalOnMissingBean(InvalidSessionStrategy.class)
public InvalidSessionStrategy invalidSessionStrategy(){
return new MyInvalidSessionStrategy(securityProperties.getBrowser().getSession().getSessionInvalidUrl());
}
@Bean
@ConditionalOnMissingBean(SessionInformationExpiredStrategy.class)
public SessionInformationExpiredStrategy sessionInformationExpiredStrategy(){
return new MyExpiredSessionStrategy(securityProperties.getBrowser().getSession().getSessionInvalidUrl());
}
//退出賬號(hào)處理器
@Bean
@ConditionalOnMissingBean(LogoutSuccessHandler.class)
public LogoutSuccessHandler logoutSuccessHandler(){
return new MyLogoutSuccessHandler(securityProperties.getBrowser().getSignOutUrl());
}
}
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
//自定義配置文件
@Autowired
private SecurityProperties securityProperties;
//登陸相關(guān)配置
@Autowired
private FormAuthenticationConfig formAuthenticationConfig;
//校驗(yàn)驗(yàn)證碼
@Autowired
private ValidateCodeSecurityConfig validateCodeSecurityConfig;
//手機(jī)號(hào)驗(yàn)證碼登陸方式
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
//登陸實(shí)現(xiàn)
@Autowired
private UserDetailsService userDetailsService;
//第三方登陸
@Autowired
private SpringSocialConfigurer sociaSecurityConfig;
//數(shù)據(jù)源
@Autowired
private DataSource dataSource;
//處理session失效
@Autowired
private InvalidSessionStrategy invalidSessionStrategy;
//處理最大登陸數(shù)
@Autowired
private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;
//集群session處理
@Autowired
private SessionRegistry sessionRegistry;
//退出處理請(qǐng)求
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
//記住密碼
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//啟動(dòng)自動(dòng)創(chuàng)建persistent_logins表
//tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Bean
public SessionRegistry getSessionRegistry(){
SessionRegistry sessionRegistry=new SessionRegistryImpl();
return sessionRegistry;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
formAuthenticationConfig.configure(http);
http
.csrf().disable()//關(guān)閉跨站防護(hù)
.apply(validateCodeSecurityConfig) //校驗(yàn)驗(yàn)證碼
.and()
.apply(smsCodeAuthenticationSecurityConfig)//手機(jī)驗(yàn)證碼登陸
.and()
.apply(sociaSecurityConfig) //第三方登陸
.and()
.authorizeRequests()//下面授權(quán)配置
.antMatchers(
SecurityConstants.DEFAULT_UNAUTHENTICATION_URL, //處理登陸請(qǐng)求
SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_MOBILE, //手機(jī)登陸
securityProperties.getBrowser().getLoginPage(), //登陸頁(yè)面
securityProperties.getBrowser().getSignUpUrl(), //注冊(cè)頁(yè)面
SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*", //驗(yàn)證碼
"/user/regist")//第三方注冊(cè)跟綁定
.permitAll()//login請(qǐng)求除外不需要認(rèn)證
.anyRequest()
.authenticated()//所有請(qǐng)求都需要身份認(rèn)證
.and()
.rememberMe() //記住密碼
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()) //失效時(shí)間
.userDetailsService(userDetailsService)
.and()
.sessionManagement()
.invalidSessionStrategy(invalidSessionStrategy) //session失效后的處理
.maximumSessions(securityProperties.getBrowser().getSession().getMaximumSessions()) //用戶最大登陸數(shù)
.maxSessionsPreventsLogin(securityProperties.getBrowser().getSession().isMaxSessionsPreventsLogin())//是否阻止登陸
.expiredUrl(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//用戶只能登陸一次
.expiredSessionStrategy(sessionInformationExpiredStrategy)//用戶被擠掉后的處理
.sessionRegistry(sessionRegistry)
.and()
.and()
.logout()
.logoutUrl("/sigOut")//默認(rèn)退出路徑是logOut可以自定義
.logoutSuccessHandler(logoutSuccessHandler)//處理退出到類
//.logoutSuccessUrl("/login")//退出后跳到到頁(yè)面
.deleteCookies("JSESSIONID");
}
}