今天是:
带着程序的旅程,每一行代码都是你前进的一步,每个错误都是你成长的机会,最终,你将抵达你的目的地。
title

基于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文件优化代码。

 

分享到:

专栏

类型标签

网站访问总量