今天是:
带着程序的旅程,每一行代码都是你前进的一步,每个错误都是你成长的机会,最终,你将抵达你的目的地。
基于OAuth2的SpringBoot Security新浪微博第三方登录
- 首先在新浪开放平台申请应用获取clientId 和cientSecret.
- 使用Springboot 2.2.1. 需要导入的包
<spring-boot.version>2.2.1.RELEASE</spring-boot.version>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${spring-boot.version}</version>
</dependency>
- 安全配置
@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
OAuth2ClientContext oauth2ClientContext;
@Autowired
private OAuth2ClientContextFilter oauth2ClientContextFilter;
@Autowired
SysUserService userService;
@Override
public void configure(WebSecurity web) {
// 设置不拦截规则
web.ignoring().antMatchers("/login", "/common/**","/userfiles/**", "/error/**", "/css/**", "/plugins/**", "/help/**", "/images/**","/js/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义 accessDecisionManager 访问控制器,并开启表达式语言
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.authorizeRequests()
.expressionHandler(webSecurityExpressionHandler()) // 启用 SpEL 表达式
.antMatchers("/oauth/**").permitAll()
//.anyRequest().authenticated()
.antMatchers("/","/auth/**", "/login**", "/*/website/*").permitAll()
.antMatchers("/**").authenticated(); // 指定所有的请求都需登录
// .and().exceptionHandling().accessDeniedPage("/error/403") // 指定登陆认证成功后,用户访问未授权的 URL 将跳转的 URL
// 自定义登录页面
http.formLogin().loginPage("/login") // 指定登录页面
.loginProcessingUrl("/signin") // 执行登录操作的 URL
.usernameParameter("username") // 用户请求登录提交的的用户名参数
.passwordParameter("password") // 用户请求登录提交的密码参数
.failureHandler(this.authenticationFailureHandler()) // 定义登录认证失败后执行的操作
.successHandler(this.authenticationSuccessHandler()); // 定义登录认证曾工后执行的操作
// 自定义注销
http.logout().logoutUrl("/signout") // 执行注销操作的 URL
.logoutSuccessUrl("/login") // 注销成功后跳转的页面
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID");
// session 管理
//http.sessionManagement().sessionFixation().none().maximumSessions(1);
// 禁用 CSRF
http
.csrf().disable()
.addFilterBefore(ssoFilter(null, null), BasicAuthenticationFilter.class)
.addFilterAfter(oauth2ClientContextFilter, SecurityContextPersistenceFilter.class);
}
/**
* 登录认证配置
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.userDetailsService())
.passwordEncoder(new BCryptPasswordEncoder());
}
/**
* 使用自定义的登录认证失败处理类,需继承 AuthenticationFailureHandler
*/
@Bean(name = "authenticationFailureHandlerImpl")
public AuthenticationFailureHandler authenticationFailureHandler() {
return new AuthenticationFailureHandlerImpl();
}
/**
* 使用自定义的登录认证成功处理类,需继承 AuthenticationSuccessHandler
*/
@Bean(name = "authenticationSuccessHandlerImpl")
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new AuthenticationSuccessHandlerImpl();
}
@Bean(name = "userDetailsServiceImpl")
public UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}
// 表达式控制器
@Bean(name = "expressionHandler")
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
return new DefaultWebSecurityExpressionHandler();
}
private Filter ssoFilter(ClientResources client, String path) throws Exception {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
//sina
OAuth2ClientAuthenticationProcessingFilter sinaFilter = new OAuth2ClientAuthenticationProcessingFilter("/auth/sina");
sinaFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
sinaFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler());
OAuth2RestTemplate sinaTemplate = new OAuth2RestTemplate(sina(), oauth2ClientContext);
sinaFilter.setRestTemplate(sinaTemplate);
SinaUserInfoTokenServices sinaTokenServices = new SinaUserInfoTokenServices(sinaResource().getUserInfoUri(), sina().getClientId());
sinaTokenServices.setRestTemplate(sinaTemplate);
sinaTokenServices.setUserService(userService);
sinaFilter.setTokenServices(sinaTokenServices);
filters.add(sinaFilter);
return filter;
}
@Bean
public ResourceServerProperties sinaResource() {
ResourceServerProperties resourceServerProperties = new ResourceServerProperties();
resourceServerProperties.setUserInfoUri("https://api.weibo.com/2/users/show.json");
return resourceServerProperties;
}
@Bean
public AuthorizationCodeResourceDetails sina() {
AuthorizationCodeResourceDetails resourceDetails = new AuthorizationCodeResourceDetails();
resourceDetails.setId("oauth2server");
resourceDetails.setTokenName("oauth_token");
resourceDetails.setClientId("你的 cientId");
resourceDetails.setClientSecret("你的ClientSecret");
resourceDetails.setAccessTokenUri("https://api.weibo.com/oauth2/access_token");
resourceDetails.setUserAuthorizationUri("https://api.weibo.com/oauth2/authorize");
resourceDetails.setScope(Arrays.asList("read"));
resourceDetails.setPreEstablishedRedirectUri('你的回调地址');
resourceDetails.setUseCurrentUri(false);
resourceDetails.setClientAuthenticationScheme(AuthenticationScheme.form);
return resourceDetails;
}
@Bean(BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(
OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
}
-
创建SinaUserInfoTokenServices,此类为授权成功后获取新浪用户信息,具体配置如下
@Slf4j
public class SinaUserInfoTokenServices extends UserInfoTokenServices {
private final Logger log = LoggerFactory.getLogger(getClass());
private SysUserService userService;
private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor();
private String userInfoEndpointUrl;
private String clientId;
private OAuth2RestOperations restTemplate;
public SinaUserInfoTokenServices(String userInfoEndpointUrl, String clientId) {
super(userInfoEndpointUrl, clientId);
this.userInfoEndpointUrl = userInfoEndpointUrl;
this.clientId = clientId;
}
@Override
protected Object getPrincipal(Map<String, Object> map) {
/**
* http://open.weibo.com/wiki/2/users/show
*返回值字段 字段类型 字段说明
id int64 用户UID
idstr string 字符串型的用户UID
screen_name string 用户昵称
name string 友好显示名称
province int 用户所在省级ID
city int 用户所在城市ID
location string 用户所在地
description string 用户个人描述
url string 用户博客地址
profile_image_url string 用户头像地址(中图),50×50像素
profile_url string 用户的微博统一URL地址
domain string 用户的个性化域名
weihao string 用户的微号
gender string 性别,m:男、f:女、n:未知
followers_count int 粉丝数
friends_count int 关注数
statuses_count int 微博数
favourites_count int 收藏数
created_at string 用户创建(注册)时间
following boolean 暂未支持
allow_all_act_msg boolean 是否允许所有人给我发私信,true:是,false:否
geo_enabled boolean 是否允许标识用户的地理位置,true:是,false:否
verified boolean 是否是微博认证用户,即加V用户,true:是,false:否
verified_type int 暂未支持
remark string 用户备注信息,只有在查询用户关系时才返回此字段
status object 用户的最近一条微博信息字段 详细
allow_all_comment boolean 是否允许所有人对我的微博进行评论,true:是,false:否
avatar_large string 用户头像地址(大图),180×180像素
avatar_hd string 用户头像地址(高清),高清头像原图
verified_reason string 认证原因
follow_me boolean 该用户是否关注当前登录用户,true:是,false:否
online_status int 用户的在线状态,0:不在线、1:在线
bi_followers_count int 用户的互粉数
lang string 用户当前的语言版本,zh-cn:简体中文,zh-tw:繁体中文,en:英语
*/
log.info("{}", map);
String loginName = map.get("screen_name")==null?null:map.get("screen_name").toString();
String thirdPartId = map.get("idstr")==null?null:map.get("idstr").toString();
String icon = map.get("profile_image_url")==null?null:map.get("profile_image_url").toString();
String bio = map.get("description")==null?null:map.get("description").toString();
String gender = map.get("gender")==null?null:map.get("gender").toString();
//根据自及应用结构自定义
SysUser sysUser= (SysUser) userService.getUserByUsername(loginName);
if(sysUser==null){
sysUser =new SysUser();
sysUser.setUsername(loginName);
sysUser.setPassword(loginName);
// sysUser.setEmail(email);
sysUser.setImageurl(icon);
SysUser temp= (SysUser) userService.getUserByUsername(loginName);
int id = userService.insert(sysUser);
//插入用户角色
userService.insertDefaultRole(sysUser.getId());
}
return new User(loginName,loginName,this.authoritiesExtractor.extractAuthorities(map));
}
@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
Map<String, Object> map = getMap(userInfoEndpointUrl, accessToken);
if (map.containsKey("error")) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("userinfo returned error: " + map.get("error"));
}
throw new InvalidTokenException(accessToken);
}
return extractAuthentication(map);
}
private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
Object principal = getPrincipal(map);
List<GrantedAuthority> authorities = this.authoritiesExtractor.extractAuthorities(map);
OAuth2Request request = new OAuth2Request(null, this.clientId, null, true, null, null, null, null, null);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
token.setDetails(map);
return new OAuth2Authentication(request, token);
}
@SuppressWarnings({"unchecked"})
private Map<String, Object> getMap(String path, String accessToken) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Getting user info from: " + path);
}
try {
String openIdUri = "https://api.weibo.com/2/account/get_uid.json";
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(openIdUri);
builder.queryParam("access_token", accessToken);
Map uidMap = restTemplate.getForObject(builder.build().encode().toUri(), Map.class);
/**
*callback( {"client_id":"101446208","openid":"A193D12113B979C63F73211447C84A91"} );
*/
Object uid = uidMap.get("uid");
log.info("{},openId:{}", uidMap, uid);
builder = UriComponentsBuilder.fromHttpUrl(path);
builder.queryParam("uid", uid);
builder.queryParam("access_token", accessToken);
URI userInfoUrl = builder.build().encode().toUri();
log.info("userInfoUrl:{}", userInfoUrl.toString());
Map result = restTemplate.getForEntity(userInfoUrl, Map.class).getBody();
log.info("userInfo:{}", result);
return result;
} catch (Exception ex) {
this.logger.warn("Could not fetch user details: " + ex.getClass() + ", " + ex.getMessage());
return Collections.<String, Object>singletonMap("error", "Could not fetch user details");
}
}
public void setUserService(SysUserService userService) {
this.userService = userService;
}
@Override
public void setRestTemplate(OAuth2RestOperations restTemplate) {
this.restTemplate = restTemplate;
}
}
注意上述配置完成后若出现如下错误,可在web.xml配置RequestContextListener监听器
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.oauth2ClientContext': Scope 'session' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:368)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:35)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:192)
at com.sun.proxy.$Proxy79.getAccessToken(Unknown Source)
at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:169)
at org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication(OAuth2ClientAuthenticationProcessingFilter.java:105)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:200)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:92)
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:77)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:358)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:271)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:496)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:650)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:803)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:790)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1468)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131)
at org.springframework.web.context.request.SessionScope.get(SessionScope.java:55)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:356)
... 48 more
- 总结:通过上述就可以实现新浪微博登录你的应用了。springboot security 基于OAuth2整体来说配置简洁,但需要理解OAuth2的工作原理。另外注意应用的回调地址和
OAuth2ClientAuthenticationProcessingFilter 过滤器地址需一致。另外如果使用yml文件程序可能更优雅。 后面考虑使用yml文件优化代码。
分享到: