SpringSecurity实战笔记之Social
===================Spring Social================
一、OAuth协议:
在不向第三方应用提供账号、密码的情况下,允许其访问资源所有者特定资源所使用的协议,例如微信授权登录。
最常用的有 授权码模式、密码模式
二、Spring Social基本原理:
1、SocialAuthenticationFilter将其拦截下来,并引导其完全OAuth所需的流程,然后根据用户信息构建Authentication并放入SecurityContext中;
2、授权码模式流程(A为资源所有者,B为服务提供商(同时拥有认证服务器B1,资源服务器B2),C为第三方应用):
A访问C->
(1)、C将A导向B1->
(2)、B1询问A是否同意授权->A回应B1同意->
(3)、B1携带授权码回到C->
(4)、C拿到授权码后向B1申请住令牌->
(5)、B1向C发放令牌->
(6)、C拿着令牌向B2获取用户资源
(7)、根据用户信息构建Authentication并放入SecurityContext中
(1)到(5)是OAuth协议标准流程,(6)为个性化流程
3、要实现的接口和类
3.1、与(1)到(6)步相关的接口与类
ServiceProvider(Spring Social 提供了抽象类AbstractOAuth2ServiceProvider)
OAuth2Operations(Spring Social 提供了OAuth2Template)完成(1)到(5)是OAuth协议标准流程
Api(Spring Social 提供了抽象类AbstractOAuth2ApiBinding)完成(6)所需要的接口实现
3.2、与(7)步相关的接口与类
-Connection(实现类OAuth2Connection,有固定的数据结构)包含用户信息的对象,由ConnectionFactory创建
-ConnectionFactory(实现类OAuth2ConnectionFactory)完成将获取到的用户信息转化为Connection数据结构,包含着
ServiceProvider实现
ApiAdapter实现
-UersConnectionRepository(实现类JdbcUsersConnectionRepository)操作DB UserConnection表(保存着业务表中userId与第三方用户信息对应记录)
三、QQ登录
1、QQ互联api:http://wiki.connect.qq.com/get_user_info
2、Api实现
2.1、 接口
public interface QQ
2.2、接口的实现并继承了AbstractOAuth2ApiBinding
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ
2.3、用来接收userInfo的对象
public class QQUserInfo
3、ServiceProvider实现
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ>
4、ApiAdapter实现
public class QQAdapter implements ApiAdapter<QQ>
5、ConnectionFactory实现
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ>
6、配置(相关参数放在QQProperties类中)
6.1、QQAutoConfig
@Configuration @ConditionalOnProperty(prefix = "imooc.security.social.qq",name = "app-id") public class QQAutoConfig extends SocialAutoConfigurerAdapter { @Autowired private SecurityProperties securityProperties; @Override protected ConnectionFactory<?> createConnectionFactory() { QQProperties qqConfig = securityProperties.getSocial().getQq(); return new QQConnectionFactory(qqConfig.getProviderId(),qqConfig.getAppId(),qqConfig.getAppSecret()); } }
6.2、SocialConfig(与weixin共用)
@Configuration @EnableSocial @Order(Integer.MIN_VALUE)//UsersConnectionRepository的实现类是通过取SocialConfigurer实现类list中的第一个得到,故要对想要的实现类加上Order(Integer.MIN_VALUE) public class SocialConfig extends SocialConfigurerAdapter{ @Autowired private DataSource dataSource; //ConnectionFactoryLocator 作用是查找ConnectionFactory,并判断使用哪个ConnectionFactory @Override public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) { JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText()); repository.setTablePrefix("imooc_"); if(connectionSignUp!=null){ repository.setConnectionSignUp(connectionSignUp); } return repository; } @Bean public SpringSocialConfigurer imoocSocialSecurityConfig(){ return new SpringSocialConfigurer(); } }
7、将imoocSocialSecurityConfig apply到BrowserSecurityConfig中
8、SocialUserDetailsService的实现
@Override public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException { logger.info("社交登录用户ID:"+userId); return buildUser(userId); } private SocialUserDetails buildUser(String userId) { String password = passwordEncoder.encode("123456"); logger.info("数据库密码是:"+password); return new SocialUser(userId,password, true,true,true,true,AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN")); }
9、在登录页面添加QQ登录链接<a href="/auth/qq">QQ登录</a>
9.1、/auth 是由SocialAuthenticationFilter中控制的,可以通过重写SpringSocialConfigure的postProcess方法进行自定义
9.2、/qq 是第三方商户id(providerId),完全是自定义
10、要注意地方
10.1、请求QQ授权后返回的content-type是html,但spring security默认期望返回为json格式的字符串,并通过解析json字符串得到返回的值
这件事是由OAuth2Template.createRestTemplate方法实例出来的RestTemplate做的,故要创建QQOAuth2Template(QQServiceProvider中要改为采用QQOAuth2Template)替换掉OAuth2Template.createRestTemplate的实现,让其支持html,并从返回的字符串中读取对应的字段
代码:
public class QQOAuth2Template extends OAuth2Template { Logger logger = LoggerFactory.getLogger(getClass()); public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) { super(clientId, clientSecret, authorizeUrl, accessTokenUrl); setUseParametersForClientAuthentication(true);//设为true后,将会将client_id、client_secret传过去 } //添加支持html content-type @Override protected RestTemplate createRestTemplate() { RestTemplate restTemplate = super.createRestTemplate(); restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8"))); return restTemplate; } //从返回的字符串中读取对应的字段 @Override protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) { String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class); //access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14 logger.info("获accessToken的响应:"+responseStr); String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&"); String accessToken = StringUtils.substringAfterLast(items[0],"="); Long expireIn = new Long(StringUtils.substringAfterLast(items[1],"=")); String refreshToken = StringUtils.substringAfterLast(items[2],"="); return new AccessGrant(accessToken,null,refreshToken,expireIn); } }
四、注册
1、SocialAuthenticationProvider 对 SocialAuthenticationToken 判断userId有没有值,就抛出BadCredentialsException,由SocialAuthenticationFilter进行处理:将信息保存在session中在抛出SocialAuthenticationRedirectException跳到注册页面
2、自定义注册页并使得路径可配置
application.properties中
imooc.security.browser.signUpUrl = demo-signUp.html
@Bean public SpringSocialConfigurer imoocSocialSecurityConfig(){ //使用默认的配置,拦截路径为/auth //return new SpringSocialConfigurer(); //自定义配置 String filterProcessUrl = securityProperties.getSocial().getFilterProcessUrl(); ImoocSpringSocialConfigure configure = new ImoocSpringSocialConfigure(filterProcessUrl); configure.signupUrl(securityProperties.getBrowser().getSignUpUrl());//自定义注册页面 return configure; }
3、ProviderSignInUtils工具类
//读写缓存了第三方登录信息的工具类 @Autowired private ConnectionFactoryLocator connectionFactoryLocator; @Bean public ProviderSignInUtils providerSignInUtils(){ return new ProviderSignInUtils(connectionFactoryLocator,getUsersConnectionRepository(connectionFactoryLocator)); }
然后就可以在controller中使用providerSignInUtils读写imooc_userconnection表中的数据
@Autowired private ProviderSignInUtils providerSignInUtils; //获取SocialUserInfo @GetMapping("/social/user") public SocialUserInfo getSocialUserInfo(HttpServletRequest request){ SocialUserInfo userInfo = new SocialUserInfo(); Connection<?> connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request)); userInfo.setProviderId(connection.getKey().getProviderId()); userInfo.setProviderUserId(connection.getKey().getProviderUserId()); userInfo.setHeadimg(connection.getImageUrl()); userInfo.setNickname(connection.getDisplayName()); return userInfo; } //更新imooc_userconnection @PostMapping("/regist") public Object regist(User user,HttpServletRequest request){ //不管是注册用户还是绑定用户,都会拿到用户的唯一标识 String userName = user.getUsername(); providerSignInUtils.doPostSignUp(userName,new ServletWebRequest(request)); return user; }
4、偷偷地给用户注册一个用户
实现JdbcUsersConnectionRepository的ConnectionSignUp,就可以自动注册一个用户,不必跳到注册页面
@Component public class DemoConnectionSignUp implements ConnectionSignUp { @Override public String execute(Connection<?> connection) { //根据社交用户信息默认创建用户并返回用户唯一标识 return connection.getDisplayName(); } }
安全模块中配置
@Autowired(required = false) private ConnectionSignUp connectionSignUp; @Override public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) { JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText()); repository.setTablePrefix("imooc_"); if(connectionSignUp!=null){ repository.setConnectionSignUp(connectionSignUp); } return repository; }
五、微信登录(与QQ登录相似,下面只说明一下不同点)
1、api:WeixinImpl的getUserInfo方法要传入openId,重写getMessageConverters()方法
/** * 默认注册的StringHttpMessageConverter字符集为ISO-8859-1,而微信返回的是UTF-8的,所以覆盖了原来的方法。 */ @Override protected List<HttpMessageConverter<?>> getMessageConverters() { List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters(); messageConverters.remove(0); messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8"))); return messageConverters; }
2、OAuth2Template:WeixinOAuth2Template重构了与请求url,返回结果解析相关方法,因为微信有些参数名与标准的OAuth2.0不同,详情查看代码
exchangeForAccess、refreshAccess、buildAuthenticateUrl、buildAuthorizeUrl
3、ConnectionFactory:与QQ相差很大,由于微信的openId是和accessToken一起返回的,需要重写以下方法
extractProviderUserId、createConnection,而且ApiAdapter类要每次调用都new一个,不能共用
六、绑定与解绑(此处有问题java.lang.IllegalAccessError)
1、ConnectController:提供了/connect接口,将用户的绑定情况add到Model返回的是connect/status视图。我们需要写一个connect/status view
@Component("connect/status") public class ImoocConnectionStatusView extends AbstractView{ @Autowired private ObjectMapper objectMapper; @SuppressWarnings("unchecked") @Override protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { Map<String, List<Connection<?>>> connections = (Map<String, List<Connection<?>>>) model.get("connectionMap"); Map<String, Boolean> result = new HashMap<>(); for (String key : connections.keySet()) { result.put(key, CollectionUtils.isNotEmpty(connections.get(key))); } response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(result)); } }
2、对社交账号进行绑定与解绑
使用post方式请求/connect/weixin 进行绑定,使用delete方式请求/connect/weixin 进行解绑,同时实现返回视图ImoocConnectView(因为此视图想做共用,所以不能直接使用@Component("connect/weixin"))
public class ImoocConnectView 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("connection") == null) { response.getWriter().write("<h3>解绑成功</h3>"); } else { response.getWriter().write("<h3>绑定成功</h3>"); } } }
WeixinAutoConfiguration中指定绑定成功与解绑成功的视图
@Bean({"connect/weixinConnect", "connect/weixinConnected"}) @ConditionalOnMissingBean(name = "weixinConnectedView") public View weixinConnectedView() { return new ImoocConnectView(); }
七、单机session管理
1、application.properties配置
#单位为秒,最短>60,对应代码TomcatEmbeddedServletContainerFactory类中的getSessionTimeoutInMinutes方法
server.session.timeout= 60
2、session失效时,需要特殊提示
2.1、在BrowserSecurityConfig中添加
.sessionManagement() .invalidSessionUrl("/session/invalid") .and()
2.2、视图
@GetMapping("/session/invalid") @ResponseStatus(code = HttpStatus.UNAUTHORIZED)//401 public SimpleResponse sessionInvalid(){ String message = "session 失效"; return new SimpleResponse(message); }
2.3、将"/session/invalid"配置为不需要权限认证就能访问
3、session并发控制(最常见的场景控制一个用户只有一个session)
3.1 在BrowserSecurityConfig中添加
.sessionManagement() .invalidSessionUrl("/session/invalid") .maximumSessions(1) .maxSessionsPreventsLogin(true)//当达到最大并发session数后阻止登录(默认为false) .expiredSessionStrategy(new ImoocExpiredSessionStrategy()) .and() .and()
3.2 expiredSessionStrategy策略
public class ImoocExpiredSessionStrategy implements SessionInformationExpiredStrategy{ @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { event.getResponse().setContentType("application/json;charset=UTF-8"); event.getResponse().getWriter().write("{\"message\":\"并发登录\"}"); } }
3.3、如果是自定义的user,需要重写user的toString,hashCode,equals方法
@Override public String toString() { return this.username; } @Override public int hashCode() { return username.hashCode(); } @Override public boolean equals(Object obj) { return this.toString().equals(obj.toString()); }
4、重构
4.1、BrowserSecurityConfig修改
@Autowired private InvalidSessionStrategy invalidSessionStrategy; @Autowired private ValidateCodeSecurityConfig validateCodeSecurityConfig; .sessionManagement() .invalidSessionStrategy(invalidSessionStrategy) .maximumSessions(securityProperties.getBrowser().getSession().getMaximumSessions()) .maxSessionsPreventsLogin(securityProperties.getBrowser().getSession().isMaxSessionsPreventsLogin())//当达到最大并发session数后阻止登录 .expiredSessionStrategy(sessionInformationExpiredStrategy) .and() .and()
4.2、InvalidSessionStrategy、ValidateCodeSecurityConfig类实现extends AbstractSessionStrategy implements SessionInformationExpiredStrategy并注入容器
在BrowserSecurityBeanConfig中
@Autowired private SecurityProperties securityProperties; /** * session失效时的处理策略配置 * @return */ @Bean @ConditionalOnMissingBean(InvalidSessionStrategy.class) public InvalidSessionStrategy invalidSessionStrategy(){ return new ImoocInvalidSessionStrategy(securityProperties); } /** * 并发登录导致前一个session失效时的处理策略配置 * @return */ @Bean @ConditionalOnMissingBean(SessionInformationExpiredStrategy.class) public SessionInformationExpiredStrategy sessionInformationExpiredStrategy(){ return new ImoocExpiredSessionStrategy(securityProperties); }
4.3 AbstractSessionStrategy类
八、集群session管理
1、session支持的存储类型,可以查看StoreType: REDIS,MONGO,JDBC,HAZELCAST,HASH_MAP,NONE;
2、使用redis:spring.session.store-type=redis
#spring.redis.host=localhost
#spring.redis.port=6379
3、存入redis中的对象(包括对象中的对象)都要序列化:原来的ValidateCode、ImageCode对象需要序列化
同时,ImageCode对象中BufferedImage对象并没有实现序列化,故存入redis中时,不要给BufferedImage赋值
4、使用了redis保存session后,超时与最大并发session数的功能一样生效
九、退出登录
1、默认路径:/logout
2、默认的处理逻辑
2.1、使当前session失效
2.2、清除与当前用户相关的remember-me记录
2.3、清空当前的SecurityContext
2.4、重定向到登录页面
3、配置
3.1、指定退出路径,退出成功后重定向路径,清除cookie
@Autowired private LogoutSuccessHandler logoutSuccessHandler; .logout() .logoutUrl("/signOut") //.logoutSuccessUrl("/imooc-logout.html")//此路径要添加到允许放行中 .logoutSuccessHandler(logoutSuccessHandler) .deleteCookies("JSESSIONID") .and()
3.2、LogoutSuccessHandler类
/** * 默认的退出成功处理器,如果设置了imooc.security.browser.signOutUrl,则跳到配置的地址上, * 如果没配置,则返回json格式的响应。 * * @author zhailiang * */ public class ImoocLogoutSuccessHandler implements LogoutSuccessHandler { private Logger logger = LoggerFactory.getLogger(getClass()); public ImoocLogoutSuccessHandler(String signOutSuccessUrl) { this.signOutSuccessUrl = signOutSuccessUrl; } private String signOutSuccessUrl; private ObjectMapper objectMapper = new ObjectMapper(); @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 { response.sendRedirect(signOutSuccessUrl); } } }
3.3、在BrowserSecurityBeanConfig类中将ImoocLogoutSuccessHandler注入容器中
@Bean @ConditionalOnMissingBean(LogoutSuccessHandler.class) public LogoutSuccessHandler logoutSuccessHandler(){ return new ImoocLogoutSuccessHandler(securityProperties.getBrowser().getSignOutUrl()); }
本文来自博客园,作者:咔咔皮卡丘,转载请注明原文链接:https://www.cnblogs.com/anquing/p/17641128.html