JavaWeb_(视频网址)_二、用户模块2 QQ微信
第三方登陆流程
关于SpringSecurity 和 SpringSocial第三方登陆流程
数据库中创建QQ登陆成功后记录userconnection.sql表
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);
com.Gary.betobe.config
package com.Gary.betobe.config; import javax.sql.DataSource; 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.EnableWebSecurity; 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.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.social.security.SpringSocialConfigurer; import com.Gary.betobe.handler.LoginFailureHandler; import com.Gary.betobe.handler.LoginSuccessHandler; import com.Gary.betobe.properties.SecurityProperties; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private SpringSocialConfigurer SpringSocialConfigurer; //告诉SpringSecurity我们密码使用什么加密的 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired private LoginFailureHandler loginFailureHandler; @Autowired private DataSource dataSource; @Autowired private UserDetailsService userDetailsService; @Autowired private SecurityProperties securityProperties; public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); return tokenRepository; } //做拦截 @Override protected void configure(HttpSecurity http) throws Exception { //请求授权 http.formLogin() //自己登陆页面 .loginPage("/loginBetobe") //自己的表单登陆的URL .loginProcessingUrl("/loginPage") //登陆成功的处理 .successHandler(loginSuccessHandler) //登陆失败的处理 .failureHandler(loginFailureHandler) //记住我 .and() .rememberMe() .tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(securityProperties.getRememberMeSeconds()) .userDetailsService(userDetailsService) .and() .authorizeRequests() .antMatchers("/forgotPassword","/loginForgotPass","/loginRegister","/sms","/judgeSMS","/register","/loginBetobe", "/scss/**","/layerslider/**","/layer/**","/js/**","/images/**","/fonts/**","/dist/**","/css/**","/api/**","/bower_components/**" ).permitAll() //所有请求 .anyRequest() //都需要我们身份认证 .authenticated().and() //跨站请求伪造和防护 .csrf().disable() .apply(SpringSocialConfigurer); } }
com.Gary.betobe.properties
package com.Gary.betobe.properties; public class QQProperties { //App的唯一标识 private String appId = "100550231"; private String appSecret = "69b6ab57b22f3c2fe6a6149274e3295e"; //QQ供应商 private String providerId = "callback.do"; //拦截器拦截的请求 private String filterProcessesUrl = "/qqLogin"; public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getAppSecret() { return appSecret; } public void setAppSecret(String appSecret) { this.appSecret = appSecret; } public String getProviderId() { return providerId; } public void setProviderId(String providerId) { this.providerId = providerId; } public String getFilterProcessesUrl() { return filterProcessesUrl; } public void setFilterProcessesUrl(String filterProcessesUrl) { this.filterProcessesUrl = filterProcessesUrl; } }
com.Gary.betobe.social.qq.api
package com.Gary.betobe.social.qq.api; public class OpenId { private String client_id; private String openid; public String getClient_id() { return client_id; } public void setClient_id(String client_id) { this.client_id = client_id; } public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } }
package com.Gary.betobe.social.qq.api; public interface QQ { //得到用户信息 QQUserInfo getUserInfo(); }
package com.Gary.betobe.social.qq.api; import java.io.IOException; import org.springframework.social.oauth2.AbstractOAuth2ApiBinding; import org.springframework.social.oauth2.TokenStrategy; import org.springframework.util.StringUtils; import com.fasterxml.jackson.databind.ObjectMapper; public class QQImpl extends AbstractOAuth2ApiBinding implements QQ{ //获取用户信息 private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s"; //获取用户OpenId private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s"; private ObjectMapper objectMapper = new ObjectMapper(); private String appId; private String openId; //赋值,获取openid public QQImpl(String accessToken,String appId) { //自动拼接一个参数 super(accessToken,TokenStrategy.ACCESS_TOKEN_PARAMETER); //赋值appid this.appId = appId; //赋值openid //通过url获得openid //拼接参数 String url = String.format(URL_GET_OPENID, accessToken); //发送请求 String result = getRestTemplate().getForObject(url, String.class); //处理返回值 result = StringUtils.replace(result, "callback( ", ""); result = StringUtils.replace(result, " );", ""); //callback OpenId id = null; try { id = objectMapper.readValue(result, OpenId.class); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } //赋值openid this.openId = id.getOpenid(); } @Override public QQUserInfo getUserInfo() { //拼接参数 String url = String.format(URL_GET_USERINFO, appId,openId); //发送请求 String result = getRestTemplate().getForObject(url, String.class); //处理返回值 QQUserInfo userInfo = null; try { userInfo = objectMapper.readValue(result, QQUserInfo.class); userInfo.setOpenId(openId); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return userInfo; } }
package com.Gary.betobe.social.qq.api; public class QQUserInfo { //基本信息 private String is_lost; private String province; private String city; private String year; private String constellation; private String ret; private String msg; private String nickname; //头像 private String figureurl; private String figureurl_1; private String figureurl_2; private String figureurl_qq; private String figureurl_qq_1; private String figureurl_qq_2; private String figureurl_type; //黄钻 private String gender; private String gender_type; private String is_yellow_vip; private String vip; private String yellow_vip_level; private String level; private String is_yellow_year_vip; private String openId; public String getFigureurl_qq() { return figureurl_qq; } public void setFigureurl_qq(String figureurl_qq) { this.figureurl_qq = figureurl_qq; } public String getGender_type() { return gender_type; } public void setGender_type(String gender_type) { this.gender_type = gender_type; } public String getOpenId() { return openId; } public void setOpenId(String openId) { this.openId = openId; } public String getFigureurl_type() { return figureurl_type; } public void setFigureurl_type(String figureurl_type) { this.figureurl_type = figureurl_type; } public String getIs_lost() { return is_lost; } public void setIs_lost(String is_lost) { this.is_lost = is_lost; } 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 getYear() { return year; } public void setYear(String year) { this.year = year; } public String getConstellation() { return constellation; } public void setConstellation(String constellation) { this.constellation = constellation; } 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 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_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 getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } 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; } }
com.Gary.betobe.social.qq.config
package com.Gary.betobe.social.qq.config; import org.springframework.social.security.SocialAuthenticationFilter; import org.springframework.social.security.SpringSocialConfigurer; public class GarySpringSocialConfigurer extends SpringSocialConfigurer{ private String filterProcessesUrl; public GarySpringSocialConfigurer(String filterProcessesUrl) { this.filterProcessesUrl = filterProcessesUrl; } //设置默认拦截器为qqLogin @Override protected <T> T postProcess(T object) { //获得filter SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object); //设置字段 filter.setFilterProcessesUrl(filterProcessesUrl); return (T)filter; } }
package com.Gary.betobe.social.qq.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; import org.springframework.social.UserIdSource; import org.springframework.social.config.annotation.ConnectionFactoryConfigurer; import org.springframework.social.config.annotation.EnableSocial; import org.springframework.social.config.annotation.SocialConfigurerAdapter; import org.springframework.social.security.AuthenticationNameUserIdSource; import com.Gary.betobe.properties.QQProperties; import com.Gary.betobe.properties.SecurityProperties; import com.Gary.betobe.social.qq.connection.QQConnectionFactory; @Configuration @EnableSocial @Order(2) public class QQConfig extends SocialConfigurerAdapter{ @Autowired private SecurityProperties securityProperties; @Override public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) { QQProperties qqProperties = securityProperties.getQqProperties(); //创建ConnectionFactory QQConnectionFactory connectionFactory = new QQConnectionFactory(qqProperties.getProviderId(),qqProperties.getAppId(),qqProperties.getAppSecret()); //添加ConnectionFactory connectionFactoryConfigurer.addConnectionFactory(connectionFactory); } //获取登陆人 @Override public UserIdSource getUserIdSource() { // TODO Auto-generated method stub return new AuthenticationNameUserIdSource(); } }
package com.Gary.betobe.social.qq.config; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; 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.ConnectionRepository; import org.springframework.social.connect.ConnectionSignUp; import org.springframework.social.connect.UsersConnectionRepository; import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository; import org.springframework.social.connect.web.ConnectController; import org.springframework.social.connect.web.ProviderSignInUtils; import org.springframework.social.security.SpringSocialConfigurer; import com.Gary.betobe.properties.SecurityProperties; @Configuration @EnableSocial @Order(1) public class SocialConfig extends SocialConfigurerAdapter{ @Autowired private ConnectionSignUp connectionSignUp; @Autowired private DataSource dataSource; @Autowired private SecurityProperties securityProperties; @Autowired private ConnectionFactoryLocator connectionFactoryLocator; //登陆之后,直接将QQ的数据保存在数据库,创建一个用户 @Override public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) { JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,connectionFactoryLocator,Encryptors.noOpText()); repository.setConnectionSignUp(connectionSignUp); return repository; } //改变拦截的请求/auth -> /qqLogin @Bean public SpringSocialConfigurer GarySocialSecurityConfig() { String filterProcessUrl = securityProperties.getQqProperties().getFilterProcessesUrl(); GarySpringSocialConfigurer configurer = new GarySpringSocialConfigurer(filterProcessUrl); return configurer; } //在注册的过程中,拿到了这个SpringSocial中的信息 //业务完成之后,把用户id传给了SpringSocial @Bean public ProviderSignInUtils providerSignInUtils() { return new ProviderSignInUtils(connectionFactoryLocator,getUsersConnectionRepository(connectionFactoryLocator)); } //打开ConnectController @Bean public ConnectController connectorller(ConnectionFactoryLocator connectionFactoryLocator,ConnectionRepository connectionRepository) { return new ConnectController(connectionFactoryLocator,connectionRepository); } }
com.Gary.betobe.social.qq.connection
package com.Gary.betobe.social.qq.connection; import org.apache.coyote.Adapter; import org.springframework.social.connect.ApiAdapter; import org.springframework.social.connect.ConnectionValues; import org.springframework.social.connect.UserProfile; import com.Gary.betobe.social.qq.api.QQ; import com.Gary.betobe.social.qq.api.QQUserInfo; public class QQAdapter implements ApiAdapter<QQ>{ @Override public boolean test(QQ api) { // TODO Auto-generated method stub return true; } @Override public void setConnectionValues(QQ api, ConnectionValues values) { //获取UserInfo QQUserInfo userInfo = api.getUserInfo(); //获取用户姓名 values.setDisplayName(userInfo.getNickname()); //获取头像 values.setImageUrl(userInfo.getFigureurl_qq_1()); //获取个人首页 values.setProfileUrl(null); //获取openid values.setProviderUserId(userInfo.getOpenId()); } @Override public UserProfile fetchUserProfile(QQ api) { // TODO Auto-generated method stub return null; } @Override public void updateStatus(QQ api, String message) { // TODO Auto-generated method stub } }
package com.Gary.betobe.social.qq.connection; import org.springframework.social.connect.ApiAdapter; import org.springframework.social.connect.support.OAuth2ConnectionFactory; import org.springframework.social.oauth2.OAuth2ServiceProvider; import com.Gary.betobe.social.qq.api.QQ; public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ>{ public QQConnectionFactory(String providerId,String appId,String appSecret) { super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter()); } }
package com.Gary.betobe.social.qq.connection; import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider; import org.springframework.social.oauth2.OAuth2Operations; import com.Gary.betobe.social.qq.api.QQ; import com.Gary.betobe.social.qq.api.QQImpl; import com.Gary.betobe.social.qq.template.QQOAuth2Template; public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ>{ //将用户导向认证服务器中的url地址,用户在该地址上进行授权 private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize"; //在获取令牌的时候,需要访问的url private static final String URL_ACCESSTOKEN = "https://graph.qq.com/oauth2.0/token"; private String appId; public QQServiceProvider(String appId,String appSecret) { super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESSTOKEN)); this.appId = appId; } @Override public QQ getApi(String accessToken) { // TODO Auto-generated method stub return new QQImpl(accessToken,appId); } }
com.Gary.betobe.social.qq.signup
package com.Gary.betobe.social.qq.signup; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Random; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.connect.Connection; import org.springframework.social.connect.ConnectionSignUp; import org.springframework.stereotype.Component; import com.Gary.betobe.domain.User; import com.Gary.betobe.service.UserService; @Component public class GaryConnectionSignUp implements ConnectionSignUp{ @Autowired private UserService userService; //根据社交用户的信息创建一个user,并且返回他的唯一标识 //创建一个用户 @Override public String execute(Connection<?> connection) { Date date = new Date(System.currentTimeMillis()); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); Random r = new Random(); // /images/user/bg/profile-bg.png User user = new User(null,connection.getDisplayName(),"123456",null,null,null,connection.getDisplayName(),null,null,null,null,null,"/images/user/bg/profile-bg.png","/images/user/head/"+r.nextInt(15)+".jpg",format.format(date)); userService.saveUser(user); return user.getId().toString(); } }
com.Gary.betobe.social.qq.template
package com.Gary.betobe.social.qq.template; import java.nio.charset.Charset; 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 org.thymeleaf.util.StringUtils; public class QQOAuth2Template extends OAuth2Template{ public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) { super(clientId, clientSecret, authorizeUrl, accessTokenUrl); //让我们的clientid以及clientSecret可以拼接过来 setUseParametersForClientAuthentication(true); } //text/html @Override protected RestTemplate createRestTemplate() { RestTemplate template = super.createRestTemplate(); template.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8"))); return template; } //accept_token //expires_in //refresh_token @Override protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) { //拿到这个请求获得的字符串 String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class); System.err.println("responseStr:"+responseStr); //按照&进行切割 String[] items = StringUtils.split(responseStr, "&"); String[] item = StringUtils.split(items[1], "&"); //System.err.println("item[0]:"+item[0]); //System.err.println("item[1]:"+item[1]); System.err.println("items[2]:"+items[2]); String access_token = StringUtils.replace(items[0], "access_token=", ""); Long expries_in = new Long(StringUtils.replace(items[1], "expires_in=", "")); String refresh_token = StringUtils.replace(items[2], "refresh_token=", ""); return new AccessGrant(access_token,null,refresh_token,expries_in); } }
微信登陆同理QQ登陆,只是微信登陆要去申请令牌
com.Gary.betobe.properties
package com.Gary.betobe.properties; public class WeixinProperties { private String appId = "wxd99431bbff8305a0"; private String appSecret = "60f78681d063590a469f1b297feff3c4"; //供应商 private String prividerId = "weixin"; //filter拦截的请求 private String filterProcessesUrl = "/qqLogin"; public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getAppSecret() { return appSecret; } public void setAppSecret(String appSecret) { this.appSecret = appSecret; } public String getPrividerId() { return prividerId; } public void setPrividerId(String prividerId) { this.prividerId = prividerId; } public String getFilterProcessesUrl() { return filterProcessesUrl; } public void setFilterProcessesUrl(String filterProcessesUrl) { this.filterProcessesUrl = filterProcessesUrl; } }
com.Gary.betobe.social.weixin.api
package com.Gary.betobe.social.weixin.api; public interface Weixin { WeixinUserInfo getUserInfo(String openId); }
package com.Gary.betobe.social.weixin.api; import java.io.IOException; import java.nio.charset.Charset; import java.util.List; 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; public class WeixinImpl extends AbstractOAuth2ApiBinding implements Weixin{ private static final String URL_GET_USERINFO = "https://api.weixin.qq.com/sns/userinfo?openid="; private ObjectMapper objectMapper = new ObjectMapper(); public WeixinImpl(String accessToken) { super(accessToken,TokenStrategy.ACCESS_TOKEN_PARAMETER); } // 目的,发送请求,然后获得到weixinUserinfo数据 @Override public WeixinUserInfo getUserInfo(String openId) { // 拼接自己的请求 String url = URL_GET_USERINFO + openId; // 发送 String response = getRestTemplate().getForObject(url, String.class); // 拿到返回值,将json转换为对象WeixinUserInfo WeixinUserInfo weixinUserInfo = null; try { weixinUserInfo = objectMapper.readValue(response, WeixinUserInfo.class); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return weixinUserInfo; } //解决收到的字符串是乱码 protected List<HttpMessageConverter<?>> getMessageConverters() { List<HttpMessageConverter<?>> converters = super.getMessageConverters(); converters.remove(0); converters.add(new StringHttpMessageConverter(Charset.forName("UTF-8"))); return converters; } }
package com.Gary.betobe.social.weixin.api; public class WeixinUserInfo { private String language; private String openid; private String nickname; private String sex; private String province; private String city; private String country; private String headimgurl; private String[] privilege; private String unionid; public String getLanguage() { return language; } public void setLanguage(String language) { this.language = language; } 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 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; } }
com.Gary.betobe.social.weixin.config
package com.Gary.betobe.social.weixin.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.social.config.annotation.ConnectionFactoryConfigurer; import org.springframework.social.config.annotation.EnableSocial; import org.springframework.social.config.annotation.SocialConfigurerAdapter; import com.Gary.betobe.properties.SecurityProperties; import com.Gary.betobe.properties.WeixinProperties; import com.Gary.betobe.social.weixin.connect.WeixinConnectionFactory; @Configuration @EnableSocial public class WeixinConfigurertion extends SocialConfigurerAdapter{ @Autowired private SecurityProperties securityProperties; @Override public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) { WeixinProperties weixinProperties = securityProperties.getWeixinProperties(); WeixinConnectionFactory weixinConnectionFactory = new WeixinConnectionFactory(weixinProperties.getPrividerId(), weixinProperties.getAppId(), weixinProperties.getAppSecret()); connectionFactoryConfigurer.addConnectionFactory(weixinConnectionFactory); } }
com.Gary.betobe.social.weixin.connect
package com.Gary.betobe.social.weixin.connect; import org.springframework.social.oauth2.AccessGrant; //微信特有的令牌,带openId //正常的令牌是不带openid的,所以我们要自己实现一个特有的令牌 public class WeixinAccessGrant extends AccessGrant{ private String openId; public WeixinAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn) { super(accessToken, scope, refreshToken, expiresIn); // TODO Auto-generated constructor stub } public WeixinAccessGrant() { super(""); } public String getOpenId() { return openId; } public void setOpenId(String openId) { this.openId = openId; } }
package com.Gary.betobe.social.weixin.connect; import org.springframework.social.connect.ApiAdapter; import org.springframework.social.connect.ConnectionValues; import org.springframework.social.connect.UserProfile; import com.Gary.betobe.social.weixin.api.Weixin; import com.Gary.betobe.social.weixin.api.WeixinUserInfo; public class WeixinAdapter implements ApiAdapter<Weixin>{ private String openId; public WeixinAdapter(String openId) { this.openId = openId; } public WeixinAdapter() { } @Override public boolean test(Weixin api) { // TODO Auto-generated method stub return true; } @Override public void setConnectionValues(Weixin api, ConnectionValues values) { WeixinUserInfo info = api.getUserInfo(openId); values.setDisplayName(info.getNickname()); values.setProviderUserId(info.getOpenid()); values.setImageUrl(info.getHeadimgurl()); } @Override public UserProfile fetchUserProfile(Weixin api) { // TODO Auto-generated method stub return null; } @Override public void updateStatus(Weixin api, String message) { // TODO Auto-generated method stub } }
package com.Gary.betobe.social.weixin.connect; 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; import com.Gary.betobe.social.weixin.api.Weixin; public class WeixinConnectionFactory extends OAuth2ConnectionFactory<Weixin>{ public WeixinConnectionFactory(String providerId, String appId,String appSecret) { super(providerId, new WeixinServiceProvider(appId, appSecret), new WeixinAdapter()); } @Override public Connection<Weixin> createConnection(AccessGrant accessGrant) { // TODO Auto-generated method stub return new OAuth2Connection<>( getProviderId(), extractProviderUserId(accessGrant), accessGrant.getAccessToken(), accessGrant.getRefreshToken(), accessGrant.getExpireTime(), getOAuth2ServiceProvider(), getApiAdapter()); } //获取openid @Override protected String extractProviderUserId(AccessGrant accessGrant) { if(accessGrant instanceof WeixinAccessGrant) { return ((WeixinAccessGrant)accessGrant).getOpenId(); } return null; } @Override public Connection<Weixin> createConnection(ConnectionData data) { // TODO Auto-generated method stub return super.createConnection(data); } //返回自己的ServiceProvider public OAuth2ServiceProvider<Weixin> getOAuth2ServiceProvider() { return (OAuth2ServiceProvider<Weixin>) getServiceProvider(); } //返回weixinAdapter public ApiAdapter<Weixin> getApiAdapter(String providerUserId) { return new WeixinAdapter(providerUserId); } }
package com.Gary.betobe.social.weixin.connect; import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider; import org.springframework.social.oauth2.OAuth2Operations; import com.Gary.betobe.social.weixin.api.Weixin; import com.Gary.betobe.social.weixin.api.WeixinImpl; import com.Gary.betobe.social.weixin.template.WeixinOAuth2Template; public class WeixinServiceProvider extends AbstractOAuth2ServiceProvider<Weixin>{ //获取授权码 private static final String URL_AUTHORIZE = "https://open.weixin.qq.com/connect/qrconnect"; //获取令牌 private static final String URL_ACCESS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token"; public WeixinServiceProvider(String appId,String appSecret) { super(new WeixinOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN)); } @Override public Weixin getApi(String accessToken) { return new WeixinImpl(accessToken); } }
com.Gary.betobe.social.weixin.template
package com.Gary.betobe.social.weixin.template; import java.io.IOException; import java.nio.charset.Charset; import java.util.Map; 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.Gary.betobe.social.weixin.connect.WeixinAccessGrant; import com.fasterxml.jackson.databind.ObjectMapper; public class WeixinOAuth2Template extends OAuth2Template{ private static final String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token"; private String accessTokenUrl; private String clientId; private String clientSecret; private ObjectMapper objectMapper = new ObjectMapper(); public WeixinOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) { super(clientId, clientSecret, authorizeUrl, accessTokenUrl); this.accessTokenUrl = accessTokenUrl; this.clientId = clientId; this.clientSecret = clientSecret; } //用Code换取令牌 @Override public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri, MultiValueMap<String, String> additionalParameters) { 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); } //发送请求,获取令牌 private AccessGrant getAccessToken(StringBuilder accessTokenRequestUrl) { String response = getRestTemplate().getForObject(accessTokenRequestUrl.toString(),String.class); Map<String,Object> result = null; //获得令牌 try { result = objectMapper.readValue(response, Map.class); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } WeixinAccessGrant accessToken = new WeixinAccessGrant( (String)result.get("access_token"), (String)result.get("scope"), (String)result.get("refresh_token"), new Long((Integer) result.get("expires_in")) ); // 赋值openid accessToken.setOpenId((String)result.get("openid")); return accessToken; } //解决收到的字符串是乱码 @Override protected RestTemplate getRestTemplate() { RestTemplate template = super.getRestTemplate(); template.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8"))); return template; } //当令牌失效之后,会重新授权 @Override public AccessGrant refreshAccess(String refreshToken, String scope, 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); } public String buildAuthorizeUrl(OAuth2Parameters parameters) { return buildAuthenticateUrl(parameters); } public String buildAuthenticateUrl(OAuth2Parameters parameters) { String url = super.buildAuthenticateUrl(parameters); url = url + "&appid=" + clientId + "&scope=snsapi_login"; return url; } }
在SecurityConfig.java下的configure()方法中处理并发登陆问题
.and() .sessionManagement() //session失效 .invalidSessionUrl("/session/invalid") //最多允许登陆并发人数 .maximumSessions(1) .expiredSessionStrategy(new ExpriedSessionStrategy())
package com.Gary.betobe.config; import javax.sql.DataSource; 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.EnableWebSecurity; 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.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.session.SessionInformationExpiredStrategy; import org.springframework.social.security.SpringSocialConfigurer; import com.Gary.betobe.handler.LoginFailureHandler; import com.Gary.betobe.handler.LoginSuccessHandler; import com.Gary.betobe.properties.SecurityProperties; import com.Gary.betobe.session.ExpriedSessionStrategy; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private SpringSocialConfigurer SpringSocialConfigurer; //告诉SpringSecurity我们密码使用什么加密的 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired private LoginFailureHandler loginFailureHandler; @Autowired private DataSource dataSource; @Autowired private UserDetailsService userDetailsService; @Autowired private SecurityProperties securityProperties; public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); return tokenRepository; } //做拦截 @Override protected void configure(HttpSecurity http) throws Exception { //请求授权 http.formLogin() //自己登陆页面 .loginPage("/loginBetobe") //自己的表单登陆的URL .loginProcessingUrl("/loginPage") //登陆成功的处理 .successHandler(loginSuccessHandler) //登陆失败的处理 .failureHandler(loginFailureHandler) //记住我 .and() .rememberMe() .tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(securityProperties.getRememberMeSeconds()) .userDetailsService(userDetailsService) .and() .sessionManagement() //session失效 .invalidSessionUrl("/session/invalid") //最多允许登陆并发人数 .maximumSessions(1) .expiredSessionStrategy(new ExpriedSessionStrategy()) .and() .and() .authorizeRequests() .antMatchers("/forgotPassword","/loginForgotPass","/loginRegister","/sms","/judgeSMS","/register","/loginBetobe", "/scss/**","/layerslider/**","/layer/**","/js/**","/images/**","/fonts/**","/dist/**","/css/**","/api/**","/bower_components/**" ).permitAll() //所有请求 .anyRequest() //都需要我们身份认证 .authenticated().and() //跨站请求伪造和防护 .csrf().disable() .apply(SpringSocialConfigurer); } }
处理并发登陆方法
//有相同的用户登陆,处理方法,踢出上一个用户 @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { System.out.println("并发登陆"); // 转发到/loginBetobt event.getResponse().sendRedirect("/loginBetobe"); }
package com.Gary.betobe.session; import java.io.IOException; import javax.servlet.ServletException; import org.springframework.security.web.session.SessionInformationExpiredEvent; import org.springframework.security.web.session.SessionInformationExpiredStrategy; public class ExpriedSessionStrategy implements SessionInformationExpiredStrategy{ //有相同的用户登陆,处理方法,踢出上一个用户 @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { System.out.println("并发登陆"); // 转发到/loginBetobt event.getResponse().sendRedirect("/loginBetobe"); } }
在SecurityConfig.java下的configure()方法中处理并发登陆踢出其它用户问题
.and() .and() .logout() .logoutUrl("/signOut") .logoutSuccessHandler(logoutSuccessHandler)
package com.Gary.betobe.config; import javax.sql.DataSource; 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.EnableWebSecurity; 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.logout.LogoutSuccessHandler; import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.session.SessionInformationExpiredStrategy; import org.springframework.social.security.SpringSocialConfigurer; import com.Gary.betobe.handler.LoginFailureHandler; import com.Gary.betobe.handler.LoginSuccessHandler; import com.Gary.betobe.properties.SecurityProperties; import com.Gary.betobe.session.ExpriedSessionStrategy; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private SpringSocialConfigurer SpringSocialConfigurer; //告诉SpringSecurity我们密码使用什么加密的 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired private LoginFailureHandler loginFailureHandler; @Autowired private DataSource dataSource; @Autowired private UserDetailsService userDetailsService; @Autowired private SecurityProperties securityProperties; @Autowired private LogoutSuccessHandler logoutSuccessHandler; public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); return tokenRepository; } //做拦截 @Override protected void configure(HttpSecurity http) throws Exception { //请求授权 http.formLogin() //自己登陆页面 .loginPage("/loginBetobe") //自己的表单登陆的URL .loginProcessingUrl("/loginPage") //登陆成功的处理 .successHandler(loginSuccessHandler) //登陆失败的处理 .failureHandler(loginFailureHandler) //记住我 .and() .rememberMe() .tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(securityProperties.getRememberMeSeconds()) .userDetailsService(userDetailsService) .and() .sessionManagement() //session失效 .invalidSessionUrl("/session/invalid") //最多允许登陆并发人数 .maximumSessions(1) .expiredSessionStrategy(new ExpriedSessionStrategy()) .and() .and() .logout() .logoutUrl("/signOut") .logoutSuccessHandler(logoutSuccessHandler) .and() .authorizeRequests() .antMatchers("/forgotPassword","/loginForgotPass","/loginRegister","/sms","/judgeSMS","/register","/loginBetobe", "/scss/**","/layerslider/**","/layer/**","/js/**","/images/**","/fonts/**","/dist/**","/css/**","/api/**","/bower_components/**" ).permitAll() //所有请求 .anyRequest() //都需要我们身份认证 .authenticated().and() //跨站请求伪造和防护 .csrf().disable() .apply(SpringSocialConfigurer); } }
处理并发登陆后登出方法
//有相同的用户登陆,处理方法,踢出上一个用户 @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { System.out.println("并发登陆"); // 转发到/loginBetobt event.getResponse().sendRedirect("/loginBetobe"); }
package com.Gary.betobe.session; import java.io.IOException; import javax.servlet.ServletException; import org.springframework.security.web.session.SessionInformationExpiredEvent; import org.springframework.security.web.session.SessionInformationExpiredStrategy; public class ExpriedSessionStrategy implements SessionInformationExpiredStrategy{ //有相同的用户登陆,处理方法,踢出上一个用户 @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { System.out.println("并发登陆"); // 转发到/loginBetobt event.getResponse().sendRedirect("/loginBetobe"); } }
当用户未登陆时,header.html显示Login/Registe(r登陆/注册)的标签,当用户登陆成功时,header.html显示Sign Out(登出)的标签
使用sec:authorize判断用户是否处在登陆状态,若是,则在header.html中显示Sign Out
<li sec:authorize = "isAuthenticated()"> <!-- <div sec:authentication="name"></div> --> <a th:href="@{~/signOut}">sign Out</a> </li>
若用户不处于登陆状态,则使用sec:authorize="!isAuthenticated()",将login/Register显示出来
<li class="dropdown-login" sec:authorize="!isAuthenticated()"> <a class="loginReg" data-toggle="example-dropdown" href="#">login/Register</a> </li>
绑定与解绑
在profile-settings.html页面中编写四个按钮:QQ绑定、QQ解绑、微信绑定、微信解绑
<div class="row"> <div class="large-12 columns"> <h6 class="borderBottom">Social Profile links:</h6> </div> <div class="medium-6 columns"> <label> qq: <input type="url" name="qqLink" placeholder="enter your profile link.."> </label> </div> <div class="medium-6 columns"> <label> weixin: <input type="url" name="weixinLink" placeholder="enter your profile link.."> </label> </div> </div> </div> <div class="setting-form-inner"> <button class="button expanded" type="submit" name="submit">update now</button> </div> </form> </div> <div class="heading"> <i class="fa fa-gears"></i> <h4>Social Binding</h4> </div> <div class="row"> <div class="large-12 columns"> <div class="setting-form"> <div class="setting-form-inner"> <div class="row"> <div class="large-12 columns"> <h6 class="borderBottom">User Binding:</h6> </div> <div class="large-12 columns"> <a href="javascript:void(0);" onclick="document.getElementById('QQ').submit()" style="background: #656fc3;border-bottom: 3px solid #424da9" class="button"> <i class="fa fa-qq"></i> QQ Binding </a> <a href="javascript:void(0);" onclick="qqClick()" style="background: #5b5b5b;border-bottom: 3px solid #454545" class="button"> <i class="fa fa-qq"></i> QQ UnBinding </a> </div> <form id="QQ" action="/connect/callback.do" method="post"></form> <div> </div> <div class="large-12 columns"> <a href="javascript:void(0);" onclick="document.getElementById('Weixin').submit()" style="background: #de5e05;border-bottom: 3px solid #b94f04" class="button"> <i class="fa fa-weixin"></i> Weixin Binding </a> <a href="javascript:void(0);" onclick="weixinClick()" style="background: #5b5b5b;border-bottom: 3px solid #454545" class="button"> <i class="fa fa-weixin"></i> Weixin UnBinding </a> </div> <form id="Weixin" action="/connect/weixin" method="post"></form> </div>
用户点击按钮实现绑定QQ或者解绑QQ,实际上是发送绑定或者解绑的Url,解绑提交delete请求,绑定提交一个post请求
当点击绑定QQ的<a>标签
<a href="javascript:void(0);" onclick="document.getElementById('QQ').submit()" style="background: #656fc3;border-bottom: 3px solid #424da9" class="button"> <i class="fa fa-qq"></i> QQ Binding </a>
让其通过<form>表单发送"/connect/callback.do"请求
<form id="QQ" action="/connect/callback.do" method="post"></form>
微信同理,当点击绑定微信的<a>标签时,让其发送"/connect/weixin"请求
<div class="large-12 columns"> <a href="javascript:void(0);" onclick="document.getElementById('Weixin').submit()" style="background: #de5e05;border-bottom: 3px solid #b94f04" class="button"> <i class="fa fa-weixin"></i> Weixin Binding </a> <a href="javascript:void(0);" onclick="weixinClick()" style="background: #5b5b5b;border-bottom: 3px solid #454545" class="button"> <i class="fa fa-weixin"></i> Weixin UnBinding </a> </div> <form id="Weixin" action="/connect/weixin" method="post"></form>
当点击微信解绑时,调用weixinClick()向http://www.pinzhi365.com/connect/weixin地址发送一个"DELETE"类型请求
<a href="javascript:void(0);" onclick="weixinClick()" style="background: #5b5b5b;border-bottom: 3px solid #454545" class="button"> <i class="fa fa-weixin"></i> Weixin UnBinding </a>
//微信的解绑 function weixinClick() { $.ajax({ url:"http://www.pinzhi365.com/connect/weixin", type:"DELETE", contentType:"application/json", data:"", dataType:"json", success:function(result) { alert("解绑成功!!"); }, error:function(result) { layer.msg("解绑成功!!") } }) }
<a href="javascript:void(0);" onclick="qqClick()" style="background: #5b5b5b;border-bottom: 3px solid #454545" class="button"> <i class="fa fa-qq"></i> QQ UnBinding </a> //微信的解绑 function weixinClick() { $.ajax({ url:"http://www.pinzhi365.com/connect/weixin", type:"DELETE", contentType:"application/json", data:"", dataType:"json", success:function(result) { alert("解绑成功!!"); }, error:function(result) { layer.msg("解绑成功!!") } }) }
直接点击绑定解绑,SpringSecurity需要我们指定跳转到哪个视图
通过social.binding下的ConnectionStatusView.java,告诉用户是否绑定微信,绑定QQ
@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<String,Boolean>(); for(String key:connections.keySet()) { //在前台返回一个信息,告诉用户微信是否绑定,qq是否绑定 result.put(key, !CollectionUtils.isEmpty(connections.get(key))); } response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(result)); }
package com.Gary.betobe.social.binding; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.connect.Connection; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.springframework.web.servlet.view.AbstractView; import com.fasterxml.jackson.databind.ObjectMapper; @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<String,Boolean>(); for(String key:connections.keySet()) { //在前台返回一个信息,告诉用户微信是否绑定,qq是否绑定 result.put(key, !CollectionUtils.isEmpty(connections.get(key))); } response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(result)); } }
通过访问http://www.pinzhi365.com/connect可查看用户是否绑定QQ或微信(这里已成功绑定)
当用户微信绑定成,跳转到profile-binding-success.html页面
用户扫描二维码,成功跳转到profileBindingSuccess请求
用户成功绑定微信