IntelliJ+SpringBoot项目实战(二十)--基于SpringSecurity实现Oauth2服务端和客户端

        在前面的帖子中介绍了SpringSecurity+JWT实现了认证和授权的功能。因为基于Oauth2的统一认证在项目需求中越来越多,所以有必要将OAuth2的解决方案也整合进来,这样我们的产品既可以作为一个业务系统,也可以作为一个独立的统一认证服务器。下面详细介绍如何基于SpringSecurity实现OAuth2服务,以及应用如何作为一个客户端调用OAuth2。

一、引入OAuth2依赖

        因为SpringSecurity框架已经整合到项目里了,现在我们只需要整合OAuth2的依赖。在openjweb-sys的模块中增加下面的依赖:

        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.3.RELEASE</version>
        </dependency>

        另外需要在openjweb-sys的OpenjwebSysApplication.java类中增加注解:@EnableAuthorizationServer。

        如果应用作为OAuth2 Server,Spring Security的WebSecurityConfig和之前介绍的会有所差异,为了能够在OAuth2 Server和非OAuth2 Server之间切换,也就是说根据项目需求来确定是否部署为OAuth2 Server,我们在application-dev.yml中加一个配置参数:

#is this server is oauth2 server
oauth2:
  server: true

         当设置为true时,作为 OAuth2 Server,在本节中我们设置为true,后面会介绍这个参数的使用。

二、本节需要实现的OAuth2功能

        1、实现OAuth2 Server端和OAuth2 Client端;

        2、OAuth2授权码、根据授权码请求accessToken。

        3、根据accessToken获取用户详情。

        4、获取refreshToken。

        5、客户端的Spring Security登录。

三、开发客户端管理功能

        因为OAuth2框架需要通过clientId来识别不同的客户,所以作为OAuth2服务,需要实现一个客户端管理的功能,我们可以基于Mybatis-plus实现,为了简单起见,暂实现查询功能即可,测试数据从数据库中录入。

        现在我们开发一个OAuth2客户端管理表,名为comm_sso_client_app:

CREATE TABLE `comm_sso_client_app` (
  `row_id` varchar(40) NOT NULL COMMENT '唯一行号',
  `obj_name` varchar(40) DEFAULT NULL COMMENT '名称',
  `master_row_id` varchar(40) DEFAULT NULL COMMENT '主表RowId',
  `flow_trans_id` varchar(40) DEFAULT NULL COMMENT '流程事务ID',
  `sort_no` bigint(20) DEFAULT NULL,
  `create_dt` varchar(23) DEFAULT NULL COMMENT '创建日期',
  `update_dt` varchar(23) DEFAULT NULL COMMENT '最后修改日期',
  `create_uid` varchar(32) DEFAULT NULL COMMENT '创建人',
  `update_uid` varchar(32) DEFAULT NULL COMMENT '修改人',
  `data_flg` varchar(6) DEFAULT NULL COMMENT '是否启用',
  `record_version` bigint(20) NOT NULL,
  `flow_status` varchar(16) DEFAULT NULL COMMENT '流程状态',
  `refresh_token_overdue_seconds` bigint(20) DEFAULT NULL,
  `account` varchar(80) DEFAULT NULL COMMENT '企业账号',
  `password` varchar(200) DEFAULT NULL COMMENT '登录密码',
  `client_name` varchar(255) DEFAULT NULL COMMENT '客户端名称',
  `access_token_overdue_seconds` bigint(20) DEFAULT NULL,
  `client_id` bigint(20) DEFAULT NULL,
  `call_back_url` varchar(255) DEFAULT NULL COMMENT '回调地址',
  PRIMARY KEY (`row_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of comm_sso_client_app
-- ----------------------------
INSERT INTO `comm_sso_client_app` VALUES ('qwertyuiopkmjh7890nbgfvcredsz231', null, null, null, null, '2024-11-30 11:11:11', '2024-11-30 11:11:11', 'system', 'system', '1', '0', null, null, 'zzyc', '123456', '众智益成', null, '1', 'http://localhost:8001/demo/oauth2client/callback');

        然后我们创建一个实体类:

package org.openjweb.core.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
 

import java.io.Serializable;

@Slf4j
@Data
@TableName("comm_sso_client_app")
public class CommSsoClientApp implements  Serializable {

    @TableId(type = IdType.ASSIGN_UUID)
    private String rowId;
    private String dataFlg;
    private Integer refreshTokenOverdueSeconds;
    private String account;
    private String password;
    private String clientName;
    private Integer accessTokenOverdueSeconds;
    private Long clientId;
    private String callBackUrl;
    private Long sortNo;

    @TableField(fill = FieldFill.INSERT)
    private String createDt;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String updateDt;
    @TableField(fill = FieldFill.INSERT)
    private String createUid;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String updateUid;
    @Version
    private Long recordVersion = 0L;//因为是非空,所以试着给一个初始值

}

创建CommSsoClientAppMapper.java :

package org.openjweb.core.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.openjweb.core.entity.CommSsoClientApp;
import java.util.List;

@Mapper
public interface CommSsoClientAppMapper  extends  BaseMapper<CommSsoClientApp> {

    @Select("SELECT * FROM comm_sso_client_app WHERE account = #{account}")
    CommSsoClientApp selectByAccountId(@Param("account") String account)  ;

}

开发CommSsoClientAppService:

package org.openjweb.core.service;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.openjweb.core.entity.CommSsoClientApp;
import org.openjweb.core.mapper.CommSsoClientAppMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class CommSsoClientAppService   extends ServiceImpl<CommSsoClientAppMapper, CommSsoClientApp> {
    @Autowired
    private CommSsoClientAppMapper commSsoClientAppMapper;

    public CommSsoClientApp selectByAccountId(String accountId){

        CommSsoClientApp ent = this.commSsoClientAppMapper.selectByAccountId(accountId);

        return ent;

    }
}

接下来再开发一个控制层的类,用于测试,以后也需要实现增删改查:

package org.openjweb.core.api;

import org.openjweb.core.entity.CommSsoClientApp;
import org.openjweb.core.service.CommSsoClientAppService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试地址:http://localhost:8001/demo/ssoclient/queryAccount?account=zzyc
 */
@RequestMapping("/demo/ssoclient")
@RestController
public class CommSsoClientAppApi {

    @Autowired
    private CommSsoClientAppService commSsoClientAppService;

    @RequestMapping("queryAccount")
    public String queryAccount(String account){
        CommSsoClientApp ent = this.commSsoClientAppService.selectByAccountId(account);
        if(ent!=null){
            return ent.getClientName();
        }
        return "没查到!";
    }
}

访问上面代码里的测试地址,可正常查询。

四、开发OAuth2服务端

4.1 配置一个资源服务器

        在openjweb-sys中配置一个资源服务器:


import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

//资源服务器配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter
{
    @Override
    public void configure(ResourceServerSecurityConfigurer resources)
    {
        resources.resourceId("resource-info");
    }

    @Override
    public void configure(HttpSecurity http) throws Exception
    {
        //配置拦截接口
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .requestMatchers()
                .antMatchers("/api/user/**")
                .and().authorizeRequests()
                .antMatchers("/api/user/**")
                .authenticated();
    }
}

4.2 实现OAuth2的ClientDetails

        在openjweb-sys中开发一个OAuth2ClientDetails,实现OAuth2的ClientDetails接口:

package org.openjweb.sys.config.oauth.model;


import org.openjweb.core.entity.CommSsoClientApp;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class OAuth2ClientDetails implements ClientDetails {
    private CommSsoClientApp client;

    public OAuth2ClientDetails(CommSsoClientApp client) {
        this.client = client;
    }

    @Override
    public String getClientId() {
        return client.getAccount();
    }

    @Override
    public Set<String> getResourceIds() {
        return new HashSet<>();
    }

    @Override
    public boolean isSecretRequired() {
        return true;
    }

    @Override
    public String getClientSecret() {
        return new BCryptPasswordEncoder().encode(client.getPassword());
    }

    @Override
    public boolean isScoped() {
        return true;
    }

    @Override
    public Set<String> getScope() {
        Set<String> set = new HashSet<>();
        set.add("read");

        return set;
    }

    @Override
    public Set<String> getAuthorizedGrantTypes() {
        Set<String> set = new HashSet<>();
        set.add("authorization_code");
        set.add("refresh_token");

        return set;
    }

    @Override
    public Set<String> getRegisteredRedirectUri() {
        Set<String> set = new HashSet<>();
        set.add(client.getCallBackUrl());

        return set;
    }

    @Override
    public Collection<GrantedAuthority> getAuthorities() {
        return new HashSet<>();
    }

    @Override
    public Integer getAccessTokenValiditySeconds() {
        return client.getAccessTokenOverdueSeconds();
    }

    @Override
    public Integer getRefreshTokenValiditySeconds() {
        return client.getRefreshTokenOverdueSeconds();
    }

    @Override
    public boolean isAutoApprove(String s) {
        return true;
    }

    @Override
    public Map<String, Object> getAdditionalInformation() {
        return new HashMap<>();
    }


}

        特别注意,上面代码中,如果getClientSecret()方法中这样写:

        return new BCryptPasswordEncoder().encode(client.getPassword());

会报这种错误:There is no PasswordEncoder mapped for the id "null",网上查了这个报错的原因是返回的密码的前面需要有加密算法名称带{}包裹,例如BCrypt的格式:{bcrypt}密码,所以getClientSecret()方法应这么写:

 return "{bcrypt}"+new BCryptPasswordEncoder().encode(client.getPassword());

        这个问题花了好长时间才找到,所以大家开发时要注意!另外网上好像没找到AES的对应加密算法名,所以这里可能不支持AES的加密算法表达式。

        因为OAuth2有个一个ClientDetailsService接口,所以还需要实现此接口的方法,这个ClientDetailsService会在后面的OAuth2的认证服务类中使用,另外此类使用了上面的OAuth2ClientDetails类。现在我们实现一个OAuth2ClientDetailsService:

package com.xujianjie.oauth2.provider.config.auth.service;

import com.xujianjie.oauth2.provider.config.auth.model.MyClientDetails;
import com.xujianjie.oauth2.provider.model.Client;
import com.xujianjie.oauth2.provider.service.ClientService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.stereotype.Service;

@Service
public class MyClientDetailsService implements ClientDetailsService
{
    @Autowired
    private ClientService clientService;

    @Override
    public ClientDetails loadClientByClientId(String account) throws ClientRegistrationException
    {
        Client client = clientService.findByAccount(account);
        if (client == null)
        {
            throw new ClientRegistrationException("企业客户未注册!");
        }

        return new MyClientDetails(client);
    }
}

4.3 实现OAuth2的认证服务配置类

        现在开始开发OAuth2的认证服务配置类:

package org.openjweb.sys.config.oauth;

import lombok.extern.slf4j.Slf4j;
import org.openjweb.core.service.CommUserService;
import org.openjweb.sys.config.oauth.service.OAuth2ClientDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;

import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;

//授权服务器配置
@Configuration
@EnableAuthorizationServer
@Slf4j
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter
{
    @Autowired
    private CommUserService myUserDetailsService;

    @Autowired
    private OAuth2ClientDetailsService myClientDetailsService;

    @Autowired
    //@Qualifier("authenticationManagerBean") //authenticationManagerBean有什么区别,对应WebSecurityConfig里的
    @Qualifier("authenticationManager") //

    private AuthenticationManager authenticationManager;

    //service企业认证
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(myClientDetailsService);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager).userDetailsService(myUserDetailsService);
    }
}

4.4 改造WebSecurityConfig.java

        集成OAuth2,需要对WebSecurityConfig.java进行改造。configure(HttpSecurity http)方法中,根据application-dev.yml中的配置,如果启用了OAuth2 Server则使用不同的配置,另外对于JwtAuthenticationFilter这个Bean,如果启用了OAuth2 Server ,则不能使用,所以需要对@Bean增加条件注解:

    @Bean
    @ConditionalOnExpression("'${oauth2.server}'=='false'")
    JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
        return jwtAuthenticationFilter;
    }

下面是WebSecurityConfig.java的完整代码:

package org.openjweb.sys.config;


import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.openjweb.core.service.CommUserService;
import org.openjweb.sys.auth.security.AESPasswordEncoder;
import org.openjweb.sys.auth.security.MD5PasswordEncoder;
import org.openjweb.sys.auth.security.MyAccessDecisionManager;
import org.openjweb.sys.auth.security.MyFilterInvocationSecurityMetadataSource;
import org.openjweb.sys.entry.JwtAuthenticationEntryPoint;
import org.openjweb.sys.filter.JwtAuthenticationFilter;
import org.openjweb.sys.handler.JWTLogoutSuccessHandler;
import org.openjweb.sys.handler.JwtAccessDeniedHandler;
import org.openjweb.sys.handler.LoginFailureHandler;
import org.openjweb.sys.handler.LoginSuccessHandler;
import org.openjweb.sys.provider.MyAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;

@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    CommUserService userDetailService;

    @Bean
    public PasswordEncoder passwordEncoder()
    {
        return new AESPasswordEncoder();
    }


    @Autowired
    LoginSuccessHandler loginSuccessHandler;

    @Autowired
    LoginFailureHandler loginFailureHandler;

    @Autowired
    JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    JwtAccessDeniedHandler jwtAccessDeniedHandler;

    @Autowired
    JWTLogoutSuccessHandler jwtLogoutSuccessHandler;

    @Value("${oauth2.server}")
    private boolean isOAuth2Server = false;



    private static final String[] ALLOW_URL_LIST = {
            //
            "/login",
            "/logout",
            "/captcha",
            "/favicon.ico",
            //"/api/jwt/**",
            "/api/cms/**",
            "/api/b2c/**",
            "/api/b2b2c/**",
            "/api/sns/**",
            "/api/comm/**",
            "/api/cms1/**",
            "/api/store/**",
            "/demo/**",
            "/oauth/**" //允许oauth认证的路径
    };

    //作用???暴露AuthenticationManager给其他Bean使用
    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
        //return super.authenticationManagerBean();

    }

    //这个和上面的是什么区别?能一起用吗?

    /*@Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }*/


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        
        //下面是第二阶段整合了数据库权限控制的示例
        log.info("是否配置了oauth2 server:::::");
        log.info(String.valueOf(this.isOAuth2Server));
        if(this.isOAuth2Server){
            log.info("OAUTH2模式...........");
            http.formLogin()
                    //.loginPage("/login.html")
                    .loginProcessingUrl("/login")
                    .and()
                    .authorizeRequests()
                    .antMatchers("/login.html", "/img/**","/demo/**").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .csrf().disable();
        }
        else {
            log.info("非OAUTH2模式............");
            http.authorizeRequests()
                    .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                        @Override
                        public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                            object.setSecurityMetadataSource(cfisms());
                            object.setAccessDecisionManager(cadm());

                            return object;
                        }
                    })
                    .and().formLogin()
                    //先注掉这个检查oauth认证
                    //.successHandler(loginSuccessHandler) //登录成功处理
                    .failureHandler(loginFailureHandler) //登录失败处理
                    .loginProcessingUrl("/login").permitAll()
                    //.loginProcessingUrl("/demo/jwt/login").permitAll()

                    .and()
                    .logout()
                    .logoutSuccessHandler(jwtLogoutSuccessHandler)

                    // 禁用session
                    .and()
                    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                    // 配置拦截规则
                    .and()
                    .authorizeRequests()
                    .antMatchers(ALLOW_URL_LIST).permitAll()
                    .anyRequest().authenticated()


                    // 异常处理器
                    .and()
                    .exceptionHandling()
                    //接口登录模式打开这个
                    //.authenticationEntryPoint(jwtAuthenticationEntryPoint) //这个影响登录,会导致/login登录蔬菜
                    .accessDeniedHandler(jwtAccessDeniedHandler)

                    // 配置自定义的过滤器
                    //这个jwtAuthenticationFilter 不加也执行了,是否增加了会调整多个过滤器的执行顺序
                    .and()
                    .addFilter(jwtAuthenticationFilter())
                    .logout().permitAll().and().csrf().disable();
        }
    }

    

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        if(true){
            //如果自定义AuthenticationProvider 则不使用这个
            //auth.userDetailsService(userDetailService).passwordEncoder(aesPasswordEncoder);
            //auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder());
            DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
            provider.setUserDetailsService(userDetailService);
            provider.setPasswordEncoder(passwordEncoder());
            auth.authenticationProvider(provider);
        }
        else{
            //自定义AuthenticationProvider
            auth.authenticationProvider(new MyAuthenticationProvider(userDetailService));
        }
    }

    @Bean
    MyAccessDecisionManager cadm() {
        //System.out.println("加载角色权限设置。。。。。。。。。。。。");
        return new MyAccessDecisionManager();
    }

    @Bean
    MyFilterInvocationSecurityMetadataSource cfisms() {
        //System.out.println("加载权限设置。。。。。。。。。。。。");
        return new MyFilterInvocationSecurityMetadataSource();
    }


    @Bean
    @ConditionalOnExpression("'${oauth2.server}'=='false'")
    JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
        return jwtAuthenticationFilter;
    }
}

五、OAuth2客户端开发

        现在我们在openjweb-sys中开发一个客户端测试API,主要实现:

       (1) 跳转到OAuth2 Server请求登录;

     (2)获取到授权码后,在请求头增加clientID和密码,向OAuth2 Server的/oauth/token请求登

              录。

     (3)OAuth2 Server重定向到Spring Security 的登录页,用户登录成功后返回带access-token

              的JSON。

     (4)接下来用户可带着access-token请求获取用户详细信息接口、或者调用refreshToken刷新

              token,或者在本地进行客户端的Spring Security登录。

             下面是代码:

package org.openjweb.sys.api;

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/demo/oauth2client")
@Slf4j
public class Oauth2DemoApi {
    

    @RequestMapping(value = "/login2", method = RequestMethod.GET)
    public void thirdLogin2(HttpServletResponse response) throws IOException {
        //localhost:8001/demo/oauth2client/login2
        
        String url = "http://localhost:8001/oauth/authorize?response_type=code&client_id=zzyc&redirect_uri=http://localhost:8001/demo/oauth2client/callback";
        response.sendRedirect(url);


    }

    @RequestMapping(value="callback",method = RequestMethod.GET)

    public JSONObject callback(String code){

        System.out.println("在回调中callBack中存储返回的值..............");
        System.out.println("得到的code:::");
        System.out.println(code );
        HttpHeaders headers = new HttpHeaders();
        String clientId = "zzyc";
        //注意使用自己企业的ID
        //在请求头中设置client的ID和密码
        headers.add("authorization", "Basic " + new String(Base64.encodeBase64((clientId + ":" + "123456").getBytes())));

        MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code");
        params.add("code", code);
        params.add("client_id", clientId);
        params.add("redirect_uri", "http://localhost:8001/demo/oauth2client/callback");
        String result = null;
        JSONObject resultJson = new JSONObject();
        //=http://localhost:8003/oauth/token
        try {
            log.info("oauth2请求开始..............");
            //result = new RestTemplate().postForObject("http://localhost:8003/oauth/token", new HttpEntity<>(params, headers), String.class);
            //result = new RestTemplate().postForObject("http://localhost:8003/oauth/token", new HttpEntity<>(params, headers), String.class);
            result = new RestTemplate().postForObject("http://localhost:8001/oauth/token", new HttpEntity<>(params, headers), String.class);

            if (result == null)
            {
                System.out.println("请求oauth失败,返回空................");
            }
            else{
                log.info("返回result:"+result);
            }
            JSONObject json1 = JSONObject.parseObject(result);
            String accessToken = json1.getString("access_token");
            String refreshToken = json1.getString("refresh_token");
            HttpHeaders tmpHeaders = new HttpHeaders();
            tmpHeaders.add("authorization", "Bearer " + accessToken);

            try
            {

                result = new RestTemplate().exchange("http://localhost:8001/api/user/info", HttpMethod.GET, new HttpEntity<>(tmpHeaders), String.class).getBody();
                log.info("返回的用户信息result::::::");
                log.info(result);

                //将第三方用户写入本系统中
                JSONObject json = JSONObject.parseObject(result);//用户基本信息
                //演示refreshtoken
                HttpHeaders tmpHeaders2 = new HttpHeaders();
                tmpHeaders2.add("authorization", "Basic " + new String(Base64.encodeBase64((clientId + ":" + "123456").getBytes())));
                //用这个不行
                //tmpHeaders2.set("Authorization", "Bearer " + accessToken);

                // 准备请求体参数
                Map<String, String> params1 = new HashMap<>();
                params1.put("grant_type", "refresh_token");//refresh_token
                params1.put("refresh_token", refreshToken);
                params1.put("scope", "read"); //scope参数同样用于定义新的访问令牌的权限范围,确保客户端只能访问其被授权的资源‌
                // 发送请求,获取响应
                // 创建请求实体,设置请求头和方法
                HttpEntity<String> requestEntity = new HttpEntity<>(tmpHeaders2);
                ResponseEntity<String> response = new RestTemplate().exchange(
                        "http://localhost:8001/oauth/token?grant_type=refresh_token&refresh_token="+refreshToken,
                        HttpMethod.POST,//不能用GET
                        requestEntity, //请求实体,包含请求头和请求体
                        String.class,
                        params1 //URL中的值,没加成功
                );
                if(response==null){
                    log.info("response为空!!!!!");
                }
                else{
                    log.info("response非空!!!!");
                }

                String newResult = response.getBody();
                log.info("新获取的token:");
                log.info(newResult);


                return json;
               /* 向本地插入新用户...

                */
            }
            catch (RestClientResponseException e)
            {
                e.printStackTrace();
            }
        }
        catch (RestClientResponseException e)
        {
            e.printStackTrace();
        }

        return resultJson;

    }


}

        测试流程:

       (1)访问http://localhost:8001/demo/oauth2client/login2 开始测试,此方法将请求重定向到了OAuth2 Server:

        http://localhost:8001/oauth/authorize?response_type=code&client_id=zzyc&redirect_uri=http://localhost:8001/demo/oauth2client/callback

        (2)OAuth2自动重定向到了Spring Security登录页(前后端分离模式需要考虑如何改造):

输入admin,密码Hello0214@  然后点Sign in 登录。登录成功后,浏览器的地址显示为:

http://localhost:8001/demo/oauth2client/callback?code=ufk81B 就是带code授权码的地址,后续是在后台拿到授权码后,又请求了 http://localhost:8001/oauth/token ,看上面的代码,请求时在头部有client:123456 client端用户名+密码的authorization头,请求成功后,后台显示返回的数据包:

result:{"access_token":"6d37957a-7a96-43cf-876b-67aad7977447","token_type":"bearer","refresh_token":"80324e85-a819-4f34-bffb-e2d1686583c2","expires_in":43199,"scope":"read"}

        然后后面演示了头部带着access_token请求OAuth2 Server的获取用户详情接口http://localhost:8001/api/user/info,返回了用户信息:{"authorities":null,"accountNonExpired":true,"accountNonLocked":true,"credentialsNonExpired":false,"enabled":true,"userId":1,"rowId":"1221212121ssss","loginId":"admin",...}

        在实际项目中,可以考虑将OAuth2 Server端登录成功,但本地还没有的用户自动加到本地的用户表中。另外在项目开发中经常需要重新获取token,上面的代码中有调用refreshtoken重新获取token的代码:

        

                Map<String, String> params1 = new HashMap<>();
                params1.put("grant_type", "refresh_token");//refresh_token
                params1.put("refresh_token", refreshToken);
                params1.put("scope", "read"); //scope参数同样用于定义新的访问令牌的权限范围,确保客户端只能访问其被授权的资源‌
                // 发送请求,获取响应
                // 创建请求实体,设置请求头和方法
                HttpEntity<String> requestEntity = new HttpEntity<>(tmpHeaders2);
                ResponseEntity<String> response = new RestTemplate().exchange(
                        "http://localhost:8001/oauth/token?grant_type=refresh_token&refresh_token="+refreshToken,
                        HttpMethod.POST,//不能用GET
                        requestEntity, //请求实体,包含请求头和请求体
                        String.class,
                        params1 //URL中的值,没加成功
                );

测试刷新token返回的json:

新获取的token:
2024-12-01 21:58:58.125  INFO 4576 --- [io-8001-exec-10] org.openjweb.sys.api.Oauth2DemoApi       : {"access_token":"f0f8ae6e-8ba2-4c9c-ab60-cbb2c3db8f76","token_type":"bearer","refresh_token":"80324e85-a819-4f34-bffb-e2d1686583c2","expires_in":43199,"scope":"read"}

【客户端SpringSecurity登录】

        目前只是在服务器端登录拿到了access_token,客户端实际还没有登录,在获取用户详情后,可以参考下面的代码实现本地SpringSecurity登录:

    @Autowired
    CommUserService sysUserService;

    @Autowired
    LoginSuccessHandler loginSuccessHandler;
    。。。。。。
                //在获取用户详情后
                String loginId = json.getString("loginId");//登录用户,json是上面代码获取用户详情
                CommUser sysUser = sysUserService.selectUserByLoginId(loginId);

                // 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的JWT,实现自动登录
                UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginId, null, sysUserService.getUserAuthority(sysUser.getLoginId()));
                SecurityContextHolder.getContext().setAuthentication(token);


                ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                try {
                    loginSuccessHandler.onAuthenticationSuccess(sra.getRequest(), sra.getResponse(), token);
                }
                catch(Exception ex){}

本文代码量比较大,大家可从github下载完整代码:GitHub - openjweb/cloud at masterOpenJWeb is a java bases low code platform. Contribute to openjweb/cloud development by creating an account on GitHub.https://github.com/openjweb/cloud/tree/master

        

上一篇:WPF的表格控件 FlexGrid设置行的高度自适应


下一篇:【机器学习】入门机器学习:从理论到代码实践