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

    }
}


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