美文网首页
Spring Security项目第三方登陆(四)

Spring Security项目第三方登陆(四)

作者: 郭少华 | 来源:发表于2019-06-11 22:27 被阅读0次

    OAuth协议

    image.png

    OAuth协议中的授权模式

    • 授权码模式(authorization code)
    • 密码模式(resource owner password credentials)
    • 客户端模式(client credentials)
    • 简化模式(implicit)

    授权码模式

    image.png

    spring social基本原理

    image.png image.png

    QQ登陆

    返回User封装好到对象

    public class QQUserInfo  {
        /**
         *QQ
         */
        private String openId;
    
        private String constellation;
    
        private String is_lost;
    
        /**
         *  返回码
         */
        private String ret;
        /**
         * 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。
         */
        private String msg;
    
        /**
         *  用户在QQ空间的昵称。
         */
        private String nickname;
        /**
         * 出生年
         */
        private String year;
    
        /**
         * 性别默认男
         */
        String gender;
        /**
         * 省
         */
        String province;
    
        /**
         * 市
         */
        private String city;
    
        private String figureurl_type;
    
        /**
         *  大小为30×30像素的QQ空间头像URL。
         */
        private String figureurl;
        /**
         *  大小为50×50像素的QQ空间头像URL。
         */
        private String figureurl_1;
    
        /**
         *  大小为100×100像素的QQ空间头像URL。
         */
        private String figureurl_2;
    
    
        private String figureurl_qq;
        /**
         *  大小为40×40像素的QQ头像URL。
         */
        private String figureurl_qq_1;
        /**
         *  大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100×100的头像,但40×40像素则是一定会有。
         */
        private String figureurl_qq_2;
    
        /**
         *  标识用户是否为黄钻用户(0:不是;1:是)。
         */
        private String is_yellow_vip;
        /**
         *  标识用户是否为黄钻用户(0:不是;1:是)
         */
        private String vip;
        /**
         *  黄钻等级
         */
        private String yellow_vip_level;
        /**
         *  黄钻等级
         */
        private String level;
        /**
         * 标识是否为年费黄钻用户(0:不是; 1:是)
         */
        private String is_yellow_year_vip;
    
        public String getOpenId() {
            return openId;
        }
    
        public void setOpenId(String openId) {
            this.openId = openId;
        }
    
        public String getIs_lost() {
            return is_lost;
        }
    
        public void setIs_lost(String is_lost) {
            this.is_lost = is_lost;
        }
    
        public String getRet() {
            return ret;
        }
    
        public void setRet(String ret) {
            this.ret = ret;
        }
    
        public String getMsg() {
            return msg;
        }
    
        public void setMsg(String msg) {
            this.msg = msg;
        }
    
        public String getNickname() {
            return nickname;
        }
    
        public void setNickname(String nickname) {
            this.nickname = nickname;
        }
    
        public String getYear() {
            return year;
        }
    
        public void setYear(String year) {
            this.year = year;
        }
    
        public String getGender() {
            return gender;
        }
    
        public void setGender(String gender) {
            this.gender = gender;
        }
    
        public String getProvince() {
            return province;
        }
    
        public void setProvince(String province) {
            this.province = province;
        }
    
        public String getCity() {
            return city;
        }
    
        public void setCity(String city) {
            this.city = city;
        }
    
        public String getFigureurl() {
            return figureurl;
        }
    
        public void setFigureurl(String figureurl) {
            this.figureurl = figureurl;
        }
    
        public String getFigureurl_1() {
            return figureurl_1;
        }
    
        public void setFigureurl_1(String figureurl_1) {
            this.figureurl_1 = figureurl_1;
        }
    
        public String getFigureurl_2() {
            return figureurl_2;
        }
    
        public void setFigureurl_2(String figureurl_2) {
            this.figureurl_2 = figureurl_2;
        }
    
        public String getFigureurl_qq() {
            return figureurl_qq;
        }
    
        public void setFigureurl_qq(String figureurl_qq) {
            this.figureurl_qq = figureurl_qq;
        }
    
        public String getFigureurl_qq_1() {
            return figureurl_qq_1;
        }
    
        public void setFigureurl_qq_1(String figureurl_qq_1) {
            this.figureurl_qq_1 = figureurl_qq_1;
        }
    
        public String getFigureurl_qq_2() {
            return figureurl_qq_2;
        }
    
        public void setFigureurl_qq_2(String figureurl_qq_2) {
            this.figureurl_qq_2 = figureurl_qq_2;
        }
    
        public String getIs_yellow_vip() {
            return is_yellow_vip;
        }
    
        public void setIs_yellow_vip(String is_yellow_vip) {
            this.is_yellow_vip = is_yellow_vip;
        }
    
        public String getVip() {
            return vip;
        }
    
        public void setVip(String vip) {
            this.vip = vip;
        }
    
        public String getYellow_vip_level() {
            return yellow_vip_level;
        }
    
        public void setYellow_vip_level(String yellow_vip_level) {
            this.yellow_vip_level = yellow_vip_level;
        }
    
        public String getLevel() {
            return level;
        }
    
        public void setLevel(String level) {
            this.level = level;
        }
    
        public String getIs_yellow_year_vip() {
            return is_yellow_year_vip;
        }
    
        public void setIs_yellow_year_vip(String is_yellow_year_vip) {
            this.is_yellow_year_vip = is_yellow_year_vip;
        }
    
        public String getConstellation() {
            return constellation;
        }
    
        public void setConstellation(String constellation) {
            this.constellation = constellation;
        }
    
        public String getFigureurl_type() {
            return figureurl_type;
        }
    
        public void setFigureurl_type(String figureurl_type) {
            this.figureurl_type = figureurl_type;
        }
    
    public interface QQ {
    
        /**
         * 获取用户信息
         * @return
         */
        public QQUserInfo getUserInfo();
    }
    
    public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
        private Logger logger= LoggerFactory.getLogger(getClass());
    
        /**
         * 获取openId
         */
        private static final String URL_GET_OPENID="https://graph.qq.com/oauth2.0/me?access_token=%s";
    
        /**
         * 获取用户信息
         */
        private static final String URL_GET_USERINFO="https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";
    
    
        //QQ互联应用id
        private String appId;
    
        //QQ号码
        private String openId;
    
        private ObjectMapper objectMapper=new ObjectMapper();
    
    
        public QQImpl(String accessToken,String appId){
            /**
             * TokenStrategy.ACCESS_TOKEN_PARAMETER 表示将accessToken放到传入参数里面
             */
            super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
            this.appId=appId;
    
            String url=String.format(URL_GET_OPENID,accessToken);
            String result=getRestTemplate().getForObject(url,String.class);
            logger.info(result);
            this.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
            logger.info("openid"+openId);
    
        }
    
        /**
         * 获取用户信息
         * @return
         * @throws IOException
         */
        @Override
        public QQUserInfo getUserInfo()  {
            String url=String.format(URL_GET_USERINFO,appId,openId);
            String result=getRestTemplate().getForObject(url,String.class);
            logger.info(result);
            QQUserInfo qqUserInfo=null;
            try {
                 qqUserInfo=objectMapper.readValue(result,QQUserInfo.class);
                 qqUserInfo.setOpenId(openId);
                 return qqUserInfo;
            } catch (IOException e) {
                throw new RuntimeException("获取用户信息失败");
            }
    
        }
    }
    
    @Configuration
    //如果没配置appId与appSecret择配置类不生效
    @ConditionalOnProperty(prefix = "guosh.security.social.qq",name = {"appId","appSecret"})
    public class QQAutoConfig extends SocialAutoConfigurerAdapter {
        @Autowired
        private SecurityProperties securityProperties;
    
        @Override
        protected ConnectionFactory<?> createConnectionFactory() {
            QQProperties qqconfig= securityProperties.getSocial().getQq();
            //QQ连接工厂
            return new QQConnectionFactory(qqconfig.getProviderId(),qqconfig.getAppId(),qqconfig.getAppSecret());
        }
    }
    
    public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
    
        public QQConnectionFactory(String providerId, String appId, String appSecret) {
            super(providerId, new QQServerProvider(appId,appSecret), new QQAdapter());
        }
    }
    
    
    public class QQServerProvider extends AbstractOAuth2ServiceProvider<QQ> {
    
        private String appId;
    
        //导向认证服务器
        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 QQServerProvider(String appId,String appSecret) {
            super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
            this.appId = appId;
        }
    
        @Override
        public QQ getApi(String accessToken) {
            return new QQImpl(accessToken,appId);
        }
    }
    
    public class QQOAuth2Template extends OAuth2Template {
    
        private Logger logger= LoggerFactory.getLogger(getClass());
    
        public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
            super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
            setUseParametersForClientAuthentication(true);
        }
    
        @Override
        protected RestTemplate createRestTemplate() {
            RestTemplate restTemplate= super.createRestTemplate();
            restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
            return restTemplate;
        }
    
        /**
         *处理返回的QQ token格式
         * @param accessTokenUrl
         * @param parameters
         * @return
         */
        @Override
        protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
            String responseStr= getRestTemplate().postForObject(accessTokenUrl,parameters,String.class);
            logger.info("获取accessToken响应"+responseStr);
            //分割字符串
            String[]items=StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr,"&");
            //token
            String accessToken = StringUtils.substringAfterLast(items[0], "=");
            Long expiresIn = new Long(StringUtils.substringAfterLast(items[1], "="));
            //刷新令牌
            String refreshToken = StringUtils.substringAfterLast(items[2], "=");
    
    
            return new AccessGrant(accessToken,null,refreshToken,expiresIn);
        }
    }
    
    public class QQAdapter implements ApiAdapter<QQ> {
    
        //测试Api是否可用
        @Override
        public boolean test(QQ qq) {
            return true;
        }
    
        @Override
        public void setConnectionValues(QQ api, ConnectionValues values) {
            QQUserInfo userInfo=api.getUserInfo();
            //名称
            values.setDisplayName(userInfo.getNickname());
            //头像
            values.setImageUrl(userInfo.getFigureurl_qq());
            //主页
            values.setProfileUrl(null);
            //openId
            values.setProviderUserId(userInfo.getOpenId());
    
        }
    
        @Override
        public UserProfile fetchUserProfile(QQ qq) {
            return null;
        }
    
        @Override
        public void updateStatus(QQ qq, String s) {
            //发送消息更新动态
        }
    }
    
    public class MySpringSocialConfigurer extends SpringSocialConfigurer {
        private String filtertProcessesUrl;
    
        public MySpringSocialConfigurer(String filtertProcessesUrl) {
            this.filtertProcessesUrl = filtertProcessesUrl;
        }
    
        /**
         * 第三方认证回调路径
         * @param object
         * @param <T>
         * @return
         */
        @Override
        protected <T> T postProcess(T object) {
            SocialAuthenticationFilter filter= (SocialAuthenticationFilter) super.postProcess(object);
            filter.setFilterProcessesUrl(filtertProcessesUrl);
            return (T) filter;
        }
    }
    
    @Configuration
    @EnableSocial
    public class SocialConfig extends SocialConfigurerAdapter {
    
        @Autowired
        private DataSource dataSource;
    
        @Autowired
        private SecurityProperties securityProperties;
    
    
        @Autowired(required = false)
        private ConnectionSignUp connectionSignUp;
    
        @Override
        public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
            //把互联数据存储到表
            JdbcUsersConnectionRepository repository= new JdbcUsersConnectionRepository(dataSource,connectionFactoryLocator, Encryptors.noOpText());
            repository.setTablePrefix("sys_");
            //把qq读区出来的数据自动注册
            if(connectionSignUp!=null){
                repository.setConnectionSignUp(connectionSignUp);
            }
    
            return repository;
        }
    
        @Bean
        public SpringSocialConfigurer sociaSecurityConfig(){
            String filtertProcessesUrl=securityProperties.getSocial().getFiltertProcessesUrl();
            MySpringSocialConfigurer configurer=new MySpringSocialConfigurer(filtertProcessesUrl);
            configurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());//指定到注册页面
            return configurer;
        }
        //免注册
        @Bean
        public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator){
            return new ProviderSignInUtils(connectionFactoryLocator,getUsersConnectionRepository(connectionFactoryLocator));
        }
    }
    

    微信登陆

    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;
        /**
         * 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
         */
        private String headimgurl;
        /**
         * 用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
         */
        private String[] privilege;
        /**
         * 用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的。
         */
        private String unionid;
    
        public String getOpenid() {
            return openid;
        }
    
        public void setOpenid(String openid) {
            this.openid = openid;
        }
    
        public String getNickname() {
            return nickname;
        }
    
        public void setNickname(String nickname) {
            this.nickname = nickname;
        }
    
        public String getLanguage() {
            return language;
        }
    
        public void setLanguage(String language) {
            this.language = language;
        }
    
        public String getSex() {
            return sex;
        }
    
        public void setSex(String sex) {
            this.sex = sex;
        }
    
        public String getProvince() {
            return province;
        }
    
        public void setProvince(String province) {
            this.province = province;
        }
    
        public String getCity() {
            return city;
        }
    
        public void setCity(String city) {
            this.city = city;
        }
    
        public String getCountry() {
            return country;
        }
    
        public void setCountry(String country) {
            this.country = country;
        }
    
        public String getHeadimgurl() {
            return headimgurl;
        }
    
        public void setHeadimgurl(String headimgurl) {
            this.headimgurl = headimgurl;
        }
    
        public String[] getPrivilege() {
            return privilege;
        }
    
        public void setPrivilege(String[] privilege) {
            this.privilege = privilege;
        }
    
        public String getUnionid() {
            return unionid;
        }
    
        public void setUnionid(String unionid) {
            this.unionid = unionid;
        }
    }
    
    
    
    public interface Weixin {
        /**
         * 获取用户信息
         * @return
         */
        public WeixinUserInfo getUserInfo(String openId);
    }
    
    
    public class WeixinImpl extends AbstractOAuth2ApiBinding implements Weixin{
    
        private Logger logger= LoggerFactory.getLogger(getClass());
    
    
        /**
         *
         */
        private ObjectMapper objectMapper = new ObjectMapper();
        /**
         * 获取用户信息的url
         */
        private static final String URL_GET_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?openid=";
    
        /**
         * @param accessToken
         */
        public WeixinImpl(String accessToken) {
            super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
        }
    
        /**
         * 默认注册的StringHttpMessageConverter字符集为ISO-8859-1,而微信返回的是UTF-8的,所以覆盖了原来的方法。
         */
        protected List<HttpMessageConverter<?>> getMessageConverters() {
            List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters();
            messageConverters.remove(0);
            messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
            return messageConverters;
        }
    
        /**
         * 用户信息获取
         * @param openId
         * @return
         */
        @Override
        public WeixinUserInfo getUserInfo(String openId) {
            String url = URL_GET_USER_INFO + openId;
            String response = getRestTemplate().getForObject(url, String.class);
            logger.info(response);
            if(StringUtils.contains(response, "errcode")) {
                return null;
            }
            WeixinUserInfo profile = null;
            try {
                profile = objectMapper.readValue(response, WeixinUserInfo.class);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return profile;
        }
    }
    
    public class WeixinAccessGrant extends AccessGrant {
        
        /**
         * 
         */
        private static final long serialVersionUID = -7243374526633186782L;
        
        private String openId;
        
        public WeixinAccessGrant() {
            super("");
        }
    
        public WeixinAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn) {
            super(accessToken, scope, refreshToken, expiresIn);
        }
    
        /**
         * @return the openId
         */
        public String getOpenId() {
            return openId;
        }
    
        /**
         * @param openId the openId to set
         */
        public void setOpenId(String openId) {
            this.openId = openId;
        }
        
    }
    
    /**
     * 微信 api适配器,将微信 api的数据模型转为spring social的标准模型。
     * 
     * 
     * @author guosh
     *
     */
    public class WeixinAdapter implements ApiAdapter<Weixin> {
        
        private String openId;
        
        public WeixinAdapter() {}
        
        public WeixinAdapter(String openId){
            this.openId = openId;
        }
    
        /**
         * @param api
         * @return
         */
        @Override
        public boolean test(Weixin api) {
            return true;
        }
    
        /**
         * @param api
         * @param values
         */
        @Override
        public void setConnectionValues(Weixin api, ConnectionValues values) {
            WeixinUserInfo profile = api.getUserInfo(openId);
            values.setProviderUserId(profile.getOpenid());
            values.setDisplayName(profile.getNickname());
            values.setImageUrl(profile.getHeadimgurl());
        }
    
        /**
         * @param api
         * @return
         */
        @Override
        public UserProfile fetchUserProfile(Weixin api) {
            return null;
        }
    
        /**
         * @param api
         * @param message
         */
        @Override
        public void updateStatus(Weixin api, String message) {
            //do nothing
        }
    
    }
    
    /**
     * 微信连接工厂
     * 
     * @author guosh
     *
     */
    public class WeixinConnectionFactory extends OAuth2ConnectionFactory<Weixin> {
        
        /**
         * @param appId
         * @param appSecret
         */
        public WeixinConnectionFactory(String providerId, String appId, String appSecret) {
            super(providerId, new WeixinServiceProvider(appId, appSecret), new WeixinAdapter());
        }
        
        /**
         * 由于微信的openId是和accessToken一起返回的,所以在这里直接根据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)
         */
        public Connection<Weixin> createConnection(AccessGrant accessGrant) {
            return new OAuth2Connection<Weixin>(getProviderId(), extractProviderUserId(accessGrant), accessGrant.getAccessToken(),
                    accessGrant.getRefreshToken(), accessGrant.getExpireTime(), getOAuth2ServiceProvider(), getApiAdapter(extractProviderUserId(accessGrant)));
        }
    
        /* (non-Javadoc)
         * @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.connect.ConnectionData)
         */
        public Connection<Weixin> createConnection(ConnectionData data) {
            return new OAuth2Connection<Weixin>(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId()));
        }
        
        private ApiAdapter<Weixin> getApiAdapter(String providerUserId) {
            return new WeixinAdapter(providerUserId);
        }
        
        private OAuth2ServiceProvider<Weixin> getOAuth2ServiceProvider() {
            return (OAuth2ServiceProvider<Weixin>) getServiceProvider();
        }
    
        
    }
    
    
    /**
     * 
     * 完成微信的OAuth2认证流程的模板类。国内厂商实现的OAuth2每个都不同, spring默认提供的OAuth2Template适应不了,只能针对每个厂商自己微调。
     * 
     * @author guosh
     *
     */
    public class WeixinOAuth2Template extends OAuth2Template {
        
        private String clientId;
        
        private String clientSecret;
    
        private String accessTokenUrl;
        
        private static final String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token";
        
        private Logger logger = LoggerFactory.getLogger(getClass());
    
        public WeixinOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
            super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
            setUseParametersForClientAuthentication(true);
            this.clientId = clientId;
            this.clientSecret = clientSecret;
            this.accessTokenUrl = accessTokenUrl;
        }
        
        /* (non-Javadoc)
         * @see org.springframework.social.oauth2.OAuth2Template#exchangeForAccess(java.lang.String, java.lang.String, org.springframework.util.MultiValueMap)
         */
        @Override
        public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri,
                                             MultiValueMap<String, String> parameters) {
            
            StringBuilder accessTokenRequestUrl = new StringBuilder(accessTokenUrl);
            
            accessTokenRequestUrl.append("?appid="+clientId);
            accessTokenRequestUrl.append("&secret="+clientSecret);
            accessTokenRequestUrl.append("&code="+authorizationCode);
            accessTokenRequestUrl.append("&grant_type=authorization_code");
            accessTokenRequestUrl.append("&redirect_uri="+redirectUri);
            
            return getAccessToken(accessTokenRequestUrl);
        }
        
        public AccessGrant refreshAccess(String refreshToken, MultiValueMap<String, String> additionalParameters) {
            
            StringBuilder refreshTokenUrl = new StringBuilder(REFRESH_TOKEN_URL);
            
            refreshTokenUrl.append("?appid="+clientId);
            refreshTokenUrl.append("&grant_type=refresh_token");
            refreshTokenUrl.append("&refresh_token="+refreshToken);
            
            return getAccessToken(refreshTokenUrl);
        }
    
        @SuppressWarnings("unchecked")
        private AccessGrant getAccessToken(StringBuilder accessTokenRequestUrl) {
            
            logger.info("获取access_token, 请求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;
        }
        
        /**
         * 构建获取授权码的请求。也就是引导用户跳转到微信的地址。
         */
        public String buildAuthenticateUrl(OAuth2Parameters parameters) {
            String url = super.buildAuthenticateUrl(parameters);
            url = url + "&appid="+clientId+"&scope=snsapi_login";
            return url;
        }
        
        public String buildAuthorizeUrl(OAuth2Parameters parameters) {
            return buildAuthenticateUrl(parameters);
        }
        
        /**
         * 微信返回的contentType是html/text,添加相应的HttpMessageConverter来处理。
         */
        protected RestTemplate createRestTemplate() {
            RestTemplate restTemplate = super.createRestTemplate();
            restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
            return restTemplate;
        }
    
    }
    
    
    /**
     * 
     * 微信的OAuth2流程处理器的提供器,供spring social的connect体系调用
     * 
     * @author guosh
     *
     */
    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);
        }
    
    }
    

    绑定解绑

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>绑定解绑</title>
    </head>
    <body>
    <h2>标准绑定页面</h2>
    <!--    第一个值固定第二个是providerId   请求类型换成delete就是解除绑定-->
        <form action="/connect/weixin" method="post">
            <button type="submit">绑定微信</button>
        </form>
    </body>
    </html>
    

    查询当前账户第三方绑定情况视图

    /**
     * 返回当前账户哪些第三方登陆已经绑定(绑定与解绑)
     * @Author: Guosh
     * @Date: 2019-06-03 14:09
     */
    @Component("connect/status")
    public class ConnectionStatusView extends AbstractView {
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    
            Map<String, List<Connection<?>>> connections = (Map<String, List<Connection<?>>>) model.get("connectionMap");
    
            Map<String, Boolean> result = new HashMap<>();
            for (String key : connections.keySet()) {
                result.put(key, CollectionUtils.isNotEmpty(connections.get(key)));
            }
    
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(result));
        }
    }
    
    

    绑定与解绑

    /**
     * 绑定成功返回视图
     * @Author: Guosh
     * @Date: 2019-06-03 15:49
     */
    public class ConnetView extends AbstractView {
    
    
        @Override
        protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
            response.setContentType("text/html;charset=UTF-8");
            if (model.get("connections") == null) {
                response.getWriter().write("<h3>解绑成功</h3>");
            } else {
                response.getWriter().write("<h3>绑定成功</h3>");
            }
        }
    }
    
    @Configuration
    @ConditionalOnProperty(prefix = "guosh.security.social.weixin", name = {"appId","appSecret"})
    public class WeixinAutoConfiguration extends SocialAutoConfigurerAdapter {
    
        @Autowired
        private SecurityProperties securityProperties;
    
        /*
         * (non-Javadoc)
         * 
         * @see
         * org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter
         * #createConnectionFactory()
         */
        @Override
        protected ConnectionFactory<?> createConnectionFactory() {
            WinxinProperties weixinConfig = securityProperties.getSocial().getWeixin();
            return new WeixinConnectionFactory(weixinConfig.getProviderId(), weixinConfig.getAppId(),
                    weixinConfig.getAppSecret());
        }
    
        /**
         * 不带ed是解绑的视图带ed是绑定带视图
         * @return
         */
        @Bean({"connect/weixinConnect","connect/weixinConnected"})
        @ConditionalOnMissingBean(name = "weixinConnectedView")
        public View weixinConnectedView() {
            return new ConnetView();
        }
    
    }
    
    

    Session管理

    Session超时处理

    @Configuration
    public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
        //自定义配置文件
        @Autowired
        private SecurityProperties securityProperties;
    
        //登陆相关配置
        @Autowired
        private FormAuthenticationConfig formAuthenticationConfig;
    
        //校验验证码
        @Autowired
        private ValidateCodeSecurityConfig validateCodeSecurityConfig;
    
        //手机号验证码登陆方式
        @Autowired
        private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
    
        //登陆实现
        @Autowired
        private UserDetailsService userDetailsService;
    
        //第三方登陆
        @Autowired
        private SpringSocialConfigurer sociaSecurityConfig;
    
        //数据源
        @Autowired
        private DataSource dataSource;
    
        //处理session失效
        @Autowired
        private InvalidSessionStrategy invalidSessionStrategy;
    
        //处理最大登陆数
        @Autowired
        private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;
    
    
        //记住密码
        @Bean
        public PersistentTokenRepository persistentTokenRepository(){
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            tokenRepository.setDataSource(dataSource);
            //启动自动创建persistent_logins表
            //tokenRepository.setCreateTableOnStartup(true);
            return tokenRepository;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            formAuthenticationConfig.configure(http);
            http
                    .csrf().disable()//关闭跨站防护
                    .apply(validateCodeSecurityConfig) //校验验证码
                        .and()
                    .apply(smsCodeAuthenticationSecurityConfig)//手机验证码登陆
                        .and()
                    .apply(sociaSecurityConfig) //第三方登陆
                        .and()
                    .authorizeRequests()//下面授权配置
                        .antMatchers(
                                SecurityConstants.DEFAULT_UNAUTHENTICATION_URL, //处理登陆请求
                                SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_MOBILE, //手机登陆
                                securityProperties.getBrowser().getLoginPage(), //登陆页面
                                securityProperties.getBrowser().getSignUpUrl(), //注册页面
                                SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*", //验证码
                                "/user/regist")//第三方注册跟绑定
                                .permitAll()//login请求除外不需要认证
                        .anyRequest()
                        .authenticated()//所有请求都需要身份认证
                        .and()
                    .rememberMe() //记住密码
                        .tokenRepository(persistentTokenRepository())
                        .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()) //失效时间
                        .userDetailsService(userDetailsService)
                        .and()
                    .sessionManagement()
                        .invalidSessionStrategy(invalidSessionStrategy) //session失效后的处理
                        .maximumSessions(securityProperties.getBrowser().getSession().getMaximumSessions()) //用户最大登陆数
                        .maxSessionsPreventsLogin(securityProperties.getBrowser().getSession().isMaxSessionsPreventsLogin())//是否阻止登陆
                        .expiredUrl(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//用户只能登陆一次
                        .expiredSessionStrategy(sessionInformationExpiredStrategy);//用户被挤掉后的处理
        }
    }
    
    /**
     * @author guosh
     *
     */
    public class AbstractSessionStrategy {
    
        private final Logger logger = LoggerFactory.getLogger(getClass());
        /**
         * 跳转的url
         */
        private String destinationUrl;
        /**
         * 重定向策略
         */
        private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
        /**
         * 跳转前是否创建新的session
         */
        private boolean createNewSession = true;
        
        private ObjectMapper objectMapper = new ObjectMapper();
    
        /**
         * @param invalidSessionUrl
         */
        public AbstractSessionStrategy(String invalidSessionUrl) {
            Assert.isTrue(UrlUtils.isValidRedirectUrl(invalidSessionUrl), "url must start with '/' or with 'http(s)'");
            this.destinationUrl = invalidSessionUrl;
        }
    
        /*
         * (non-Javadoc)
         * 
         * @see org.springframework.security.web.session.InvalidSessionStrategy#
         * onInvalidSessionDetected(javax.servlet.http.HttpServletRequest,
         * javax.servlet.http.HttpServletResponse)
         */
        protected void onSessionInvalid(HttpServletRequest request, HttpServletResponse response) throws IOException {
    
            if (createNewSession) {
                request.getSession();
            }
    
            String sourceUrl = request.getRequestURI();
            String targetUrl;
    
            //if (StringUtils.endsWithIgnoreCase(sourceUrl, ".html")) {
                targetUrl = destinationUrl;
                logger.info("session失效,跳转到"+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;
        }
        
    }
    
    public class MyInvalidSessionStrategy extends AbstractSessionStrategy implements InvalidSessionStrategy {
    
        /**
         * @param invalidSessionUrl
         */
        public MyInvalidSessionStrategy(String invalidSessionUrl) {
            super(invalidSessionUrl);
        }
    
        @Override
        public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
            onSessionInvalid(request, response);
        }
    }
    
    public class MyExpiredSessionStrategy extends AbstractSessionStrategy implements SessionInformationExpiredStrategy {
    
        public MyExpiredSessionStrategy(String invalidSessionUrl) {
            super(invalidSessionUrl);
        }
    
        /* (non-Javadoc)
         * @see org.springframework.security.web.session.SessionInformationExpiredStrategy#onExpiredSessionDetected(org.springframework.security.web.session.SessionInformationExpiredEvent)
         */
        @Override
        public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
            onSessionInvalid(event.getRequest(), event.getResponse());
        }
        
        /* (non-Javadoc)
         * @see com.imooc.security.browser.session.AbstractSessionStrategy#isConcurrency()
         */
        @Override
        protected boolean isConcurrency() {
            return true;
        }
    
    }
    
    

    session集群处理

    配置文件更改

      session:
        store-type: redis
    

    退出登陆

    public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
        private String signOutSuccessUrl;
    
        private Logger logger= LoggerFactory.getLogger(getClass());
    
        private ObjectMapper objectMapper = new ObjectMapper();
    
        private RedirectStrategy redirectStrategy=new DefaultRedirectStrategy();
    
        public MyLogoutSuccessHandler(String signOutSuccessUrl) {
            this.signOutSuccessUrl = signOutSuccessUrl;
        }
    
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            logger.info("退出成功");
            if (StringUtils.isBlank(signOutSuccessUrl)) {
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse("退出成功")));
            } else {
                redirectStrategy.sendRedirect(request,response,signOutSuccessUrl);
            }
    
        }
    }
    
    
    @Configuration
    public class BrowserSecurityBeanConfig {
    
        @Autowired
        private SecurityProperties securityProperties;
        
        @Bean
        @ConditionalOnMissingBean(InvalidSessionStrategy.class)
        public InvalidSessionStrategy invalidSessionStrategy(){
            return new MyInvalidSessionStrategy(securityProperties.getBrowser().getSession().getSessionInvalidUrl());
        }
        
        @Bean
        @ConditionalOnMissingBean(SessionInformationExpiredStrategy.class)
        public SessionInformationExpiredStrategy sessionInformationExpiredStrategy(){
            return new MyExpiredSessionStrategy(securityProperties.getBrowser().getSession().getSessionInvalidUrl());
        }
    
        //退出账号处理器
        @Bean
        @ConditionalOnMissingBean(LogoutSuccessHandler.class)
        public LogoutSuccessHandler logoutSuccessHandler(){
            return new MyLogoutSuccessHandler(securityProperties.getBrowser().getSignOutUrl());
        }
        
    }
    
    
    @Configuration
    public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
        //自定义配置文件
        @Autowired
        private SecurityProperties securityProperties;
    
        //登陆相关配置
        @Autowired
        private FormAuthenticationConfig formAuthenticationConfig;
    
        //校验验证码
        @Autowired
        private ValidateCodeSecurityConfig validateCodeSecurityConfig;
    
        //手机号验证码登陆方式
        @Autowired
        private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
    
        //登陆实现
        @Autowired
        private UserDetailsService userDetailsService;
    
        //第三方登陆
        @Autowired
        private SpringSocialConfigurer sociaSecurityConfig;
    
        //数据源
        @Autowired
        private DataSource dataSource;
    
        //处理session失效
        @Autowired
        private InvalidSessionStrategy invalidSessionStrategy;
    
        //处理最大登陆数
        @Autowired
        private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;
    
        //集群session处理
        @Autowired
        private SessionRegistry sessionRegistry;
        //退出处理请求
        @Autowired
        private LogoutSuccessHandler logoutSuccessHandler;
    
        //记住密码
        @Bean
        public PersistentTokenRepository persistentTokenRepository(){
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            tokenRepository.setDataSource(dataSource);
            //启动自动创建persistent_logins表
            //tokenRepository.setCreateTableOnStartup(true);
            return tokenRepository;
        }
    
        @Bean
        public SessionRegistry getSessionRegistry(){
            SessionRegistry sessionRegistry=new SessionRegistryImpl();
            return sessionRegistry;
        }
    
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            formAuthenticationConfig.configure(http);
            http
                    .csrf().disable()//关闭跨站防护
                    .apply(validateCodeSecurityConfig) //校验验证码
                        .and()
                    .apply(smsCodeAuthenticationSecurityConfig)//手机验证码登陆
                        .and()
                    .apply(sociaSecurityConfig) //第三方登陆
                        .and()
                    .authorizeRequests()//下面授权配置
                        .antMatchers(
                                SecurityConstants.DEFAULT_UNAUTHENTICATION_URL, //处理登陆请求
                                SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_MOBILE, //手机登陆
                                securityProperties.getBrowser().getLoginPage(), //登陆页面
                                securityProperties.getBrowser().getSignUpUrl(), //注册页面
                                SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*", //验证码
                                "/user/regist")//第三方注册跟绑定
                                .permitAll()//login请求除外不需要认证
                        .anyRequest()
                        .authenticated()//所有请求都需要身份认证
                        .and()
                    .rememberMe() //记住密码
                        .tokenRepository(persistentTokenRepository())
                        .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()) //失效时间
                        .userDetailsService(userDetailsService)
                        .and()
                    .sessionManagement()
                        .invalidSessionStrategy(invalidSessionStrategy) //session失效后的处理
                        .maximumSessions(securityProperties.getBrowser().getSession().getMaximumSessions()) //用户最大登陆数
                        .maxSessionsPreventsLogin(securityProperties.getBrowser().getSession().isMaxSessionsPreventsLogin())//是否阻止登陆
                        .expiredUrl(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//用户只能登陆一次
                        .expiredSessionStrategy(sessionInformationExpiredStrategy)//用户被挤掉后的处理
                        .sessionRegistry(sessionRegistry)
                        .and()
                        .and()
                    .logout()
                        .logoutUrl("/sigOut")//默认退出路径是logOut可以自定义
                        .logoutSuccessHandler(logoutSuccessHandler)//处理退出到类
                       //.logoutSuccessUrl("/login")//退出后跳到到页面
                        .deleteCookies("JSESSIONID");
    
    
        }
    }
    
    

    相关文章

      网友评论

          本文标题:Spring Security项目第三方登陆(四)

          本文链接:https://www.haomeiwen.com/subject/lnketctx.html