【Spring Security技術棧開發企業級認證與授權】----使用Spring social開發第三方登錄:(QQ 與微信登錄)

前言

博客主要分享如何實現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登錄

API文檔
在這裏插入圖片描述

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);
        }

    }
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章