springSecurity+oauth2實現權限認證系統(資源服務器與授權服務器分離,client信息入庫,token存入redis持久化)

一、前言

本文章側重實戰,是爲線上系統做的一個demo。適合對oauth2有一定理解後再閱讀。
如果對oauth2理解還不夠深入,建議先閱讀芋道 Spring Security Oauth2入門來夯實一下基礎。
這個demo主要實現了資源服務器與授權服務器分離,client信息入庫,token信息在redis中持久化,demo的github地址會在文末貼出。

二、oAuth2授權碼模式認證流程

首先來看授權碼認證模式的一個流程圖:
在這裏插入圖片描述
這裏我們將過程分爲幾個步驟:
1.用戶訪問應用頁面
這裏我們可以理解爲用戶訪問的頁面或數據資源需要去資源服務器拿,此時我們還沒有授權,無法訪問。
2.重定向到授權頁
這個步驟我們可以主動跳轉,也可以在訪問應用頁面時判斷是否授權,如果沒授權的話就重定向到授權頁面。
3.用戶授權
我們訪問授權認證服務器,傳遞參數code,redirect_uri,response_type,scope,如果認證成功,頁面會被重定向。
4.重定向
之後頁面會重定向到一個我們在方法參數中指定的重定向地址redirect_uri,同時將code作爲參數返回。
5.獲取access_token
我們傳遞code等參數,訪問/oauth/token頁面進行認證。得到access_token,授權認證就完成了。
6.訪問資源
之後我們把access_token作爲請求頭,就可以成功訪問資源服務器的資源了。

三、權限認證系統的三個角色

通過認證流程我們已經看到了,整個權限認證系統分爲幾個角色。這裏我們把他們分爲3個角色:客戶端、認證服務器和資源服務器。
在這裏插入圖片描述
這張圖是Oauth2官方的一張規範圖。小夥伴們都發現了,這明明就是四個角色。
在這張圖中client客戶端實際指的是瀏覽器,也就是我們的用戶端。在我上面的定義中客戶端實際上是這張圖裏的Resource Owner,授權服務器是Authorization Server,資源服務器是Resource Server

四、demo實現的功能

之後我們來介紹一下在這個demo中我要實現的功能,主要有以下兩點:

1.客戶端通過授權認證後,訪問資源服務器獲取資源。
2.客戶端通過授權認證後,訪問資源服務器相應接口推送客戶端的登錄信息,之後完成資源服務器的登錄。

總結一下,我們要實現兩個功能,一個是認證之後拉數據,一個是認證之後推數據。拉數據比較好理解,就是訪問接口獲取數據,推數據的話就是我們推當前登錄人的一些用戶信息,之後資源服務器拿到數據之後判斷他是否註冊,註冊了的話就重定向到一個隱藏表單自動提交的頁面,完成登錄。
因此在這個demo中,三個項目都需要配置SpringSecurity,客戶端與資源服務器是用來登錄,認證服務器是用來集成SpringSecurityOauth2。

五、技術點

項目中用到的獨立於oauth2流程的技術點,先單獨介紹一下。

1.SpringSecurity自定義登錄頁面,賬密信息入庫

我們在項目中集成SpringSecurity,其中客戶端和資源服務器我們自定義了登錄頁面,將賬號密碼存入數據庫中,通過表單提交的方式完成登錄。
首先我們引入SpringSecurity的依賴:

<!-- security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

配置自定義登錄頁面

首先來看Config類:

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

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    .formLogin()
                    .loginPage("/portal/login")//用戶未登錄時,訪問任何資源都轉跳到該路徑,即登錄頁面
                    .loginProcessingUrl("/login")//登錄表單form中action的地址,也就是處理認證請求的路徑
                    .defaultSuccessUrl("/portal/index",true)//登錄認證成功後默認轉跳的路徑,第二個參數爲true則任何情況都跳到指定url。否則會先跳到referer,referer爲空才跳到指定url
                    .usernameParameter("username")///登錄表單form中用戶名輸入框input的name名,不修改的話默認是username
                    .passwordParameter("password")//form中密碼輸入框input的name名,不修改的話默認是password
                    .and()
                    .authorizeRequests()
                    .antMatchers("/portal/login","/portal/index").permitAll()//不需要通過登錄驗證就可以被訪問的資源路徑
                    .anyRequest().authenticated();
        }

}

上面的代碼中註釋已經很詳細了,有一個地方需要特別注意,一定要把登錄頁面設置爲不通過登錄驗證就可以訪問,否則會一直被重定向。
這裏我們指定的登錄頁面是/portal/login登錄之後重定向到首頁/portal/index,這裏我們集成了springMVC和freemarker來展示前端頁面,依賴:

<!-- WEB -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

配置文件:

spring:
  resources:
    static-locations: classpath:/
  freemarker:
    template-loader-path: classpath:/templates/
    suffix: .html
    content-type: text/html
    charset: UTF-8

把頁面放在/templates文件下就可以了。
登錄頁面代碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Title</title>
</head>
<body>
<h1>客戶端</h1>
<form action="/login" id="fm1" method="post">

    <div class="login-mmdl">
        <div class="login-input">
            <div>
                <span>用戶名:</span><input id="username" name="username" onkeyup="keyup();" placeholder="請輸入用戶名手機號" type="text" />
            </div>
            <div>
                <span>密碼:</span><input id="password" name="password" placeholder="請輸入密碼" type="password" />
            </div>
        </div>
        <button class="login-btn-dl text-white" href="javascript:;" id="username_password" onclick="login();">&nbsp;&nbsp;</button>
    </div>
</form>
</body>
<script>

function login(){
    $("fm1").submit();
}
</script>
</html>

可以看到是一個很簡單的表單提交頁面,表單的action爲/login

賬密信息入庫

引入如果沒有做登錄信息入庫,我們可以在配置文件中設置賬號密碼:

security.user.name=qbq
security.user.password=1024

但這樣顯然是和實際場景相差甚遠的,我們來配置一下將賬號密碼入庫。
首先我們引入數據庫相關依賴:

<!-- 數據庫相關 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

application.yml中配置:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/zfc_test?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    
mybatis:
  configuration:
    map-underscore-to-camel-case: true #開啓下劃線轉換駝峯命名

注意不要忘記開啓駝峯命名。
在啓動類上加MapperScan:

@SpringBootApplication
@MapperScan("com.quan.client.Mapper")
public class ClientApplication {

    public static void main(String[] args) {
        SpringApplication.run(ClientApplication.class, args);
    }

}

接下來我們建一個簡單的用戶表:

CREATE TABLE `test_user` (
  `user_name` varchar(255) NOT NULL,
  `password` varchar(255) DEFAULT NULL,
  `user_desc` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

分別是用戶名,密碼和權限,權限部分不可爲空,隨意填即可。
如下圖所示:
在這裏插入圖片描述
在剛纔的配置類中我們新增如下配置:


        @Autowired
        @Qualifier("databaseUserDetailService")
        private DataBaseUserDetailService userDetailsService;
		
		@Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService);
        }

可以看到我們使用了一個DataBaseUserDetailService類,這個類是我們定義在service層中的,代碼如下:

@Service("databaseUserDetailService")
@Transactional
public class DataBaseUserDetailService implements UserDetailsService {


    @Autowired
    private TestUserMapper testUserMapper;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        TestUser testUser = testUserMapper.findAllByUserName(userName);
        if(testUser==null){
            throw new UsernameNotFoundException("user + " + userName + "not found.");
        }
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        HttpSession session =  request.getSession();
        session.setAttribute("username",testUser.getUserName());
        session.setAttribute("password",testUser.getPassword());
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority(testUser.getUserDesc()));
        UserDetails userDetails = new User(testUser.getUserName(),testUser.getPassword(),authorities);
        return userDetails;
    }
}

我們在這個類中做的事情,就是根據我們登錄時提交的用戶名在數據庫中查詢得到用戶名、密碼和權限,之後把他們存入session並進行相應的驗證。

2.集成通用Mapper

其中tk.mybatis是一個非常好用的依賴,這裏簡單介紹一下。
依賴:

<!-- 通用Mapper -->
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>2.0.2</version>
</dependency>

引入了它我們可以在mapper中繼承父類:

public interface TestUserMapper extends Mapper<TestUser> {
}

這裏注意Mapper的泛型要是實體類,同時實體類要按規範來寫。
需要加@Entity與@Table標籤,table中的name要填數據庫中表名。每一個字段都要加@Column(name=“user_name”)註解,name是字段名,同時主鍵需要添加@Id註解。
實體類:

package com.quan.client.Entity;

import javax.persistence.*;

@Entity
@Table(name = "test_user")
public class TestUser {

    @Id
    @Column(name="user_name")
    private String userName;

    @Column(name="password")
    private String password;


    @Column(name="user_desc")
    private String userDesc;
    
	//---下面getset方法省略

}

同時我們需要在springboot的啓動類上加mapper的掃描註解:

@MapperScan("com.quan.redistest.Mapper")

注意,如果配置了上面說的tk.mybatis通用mapper,啓動類的MapperScan包需要更改爲:

import tk.mybatis.spring.annotation.MapperScan;

之後可以調用許多很實用的api,如selectByPrimaryKey(),insert()等方法來實現查詢和插入操作。

六、認證服務器

之後我們來完成認證服務器的配置。
這裏我們直接貼出授權服務器的完整配置類:

// 授權服務器配置
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Resource
    private DataSource dataSource;
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisConnectionFactory connectionFactory;
	
	//配置client信息入庫存儲
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        /*clients.inMemory() //
                //  begin ...
                .withClient("clientapp").secret("112233") // Client 賬號、密碼。
                .redirectUris("http://localhost:8080/resource/callBack") // 配置回調地址,選填。
                .authorizedGrantTypes("authorization_code","refresh_token") // 授權碼模式
                .scopes("server", "select") // 可授權的 Scope
                .accessTokenValiditySeconds(1200)
                .refreshTokenValiditySeconds(2400);*/
        //client入庫 oauth_client_details
        clients.withClientDetails(clientDetails());
    }
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                // 開啓/oauth/token_key驗證端口無權限訪問
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("permitAll()")
//        請求/oauth/token的,如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的會走ClientCredentialsTokenEndpointFilter
                .allowFormAuthenticationForClients();
    }

    @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }
	
	//配置access_token等信息持久化到redis中
    @Override
    public void configure(
            AuthorizationServerEndpointsConfigurer endpoints)
            throws Exception {
        endpoints
                .authenticationManager(authenticationManager)
                .tokenStore(tokenStore());
    }

    @Bean
    public TokenStore tokenStore(){
        final RedisTokenStore redisTokenStore = new RedisTokenStore(connectionFactory);
        //redisTokenStore.setPrefix("token-");
        return redisTokenStore;
    }
}

整個配置文件實現了兩部分功能:配置client信息入庫存儲,配置access_token等令牌信息持久化到redis中。我們分別來進行介紹。

1.配置client信息入庫存儲

在上面的配置文件中我們可以看到註釋中的內容,調用clients.inMemory()進行配置,client的信息是從內存中加載的,現在我們需要將這些信息入庫存儲,數據庫表如下:

CREATE TABLE `oauth_client_details` (
  `client_id` varchar(48) NOT NULL,
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `authorized_grant_types` varchar(256) DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

我填寫的信息如下圖所示:
在這裏插入圖片描述
我們設置id和secret,開啓了授權碼和刷新令牌兩種模式,access_token過期時間設爲3600秒。
不要遺漏下面的配置,將ClientDetailsService進行注入,既可以實現讀取數據庫表中的配置。

2.配置access_token等令牌信息持久化到redis中

如果不進行持久化配置,客戶端的access_token等信息是存儲在內存中的,如果服務掛掉所有的信息就都消失了,因此我們需要將這些信息做持久化存儲。可以選擇的方案有使用 JdbcTokenStore 持久化到關係型數據庫,和使用RedisTokenStore持久化到redis中。
因爲這些令牌信息是有過期時間的,和Redis的特性相符,同時存到redis中也可以提高授權服務器的響應速度,所以我們選擇持久化到redis中。
相應的配置已經寫在上面的配置文件的後半部分,我們還需要把redis集成到項目中。
依賴:

<!--集成redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>1.4.1.RELEASE</version>
</dependency>

配置文件:

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0
spring.redis.password=

3.授權服務器提供的一些訪問接口

①獲取code

/oauth/authorize

具體請求,需要攜帶四個參數:
client_id,redirect_uri,response_type,scope。
這四個參數需要與oauth_client_details表中存儲的client信息相對應,如有不同則會報錯,無法獲取code。

http://localhost:8080/oauth/authorize?client_id=clientapp&redirect_uri=http://www.baidu.com&response_type=code&scope=server

之後授權服務器會做一個重定向,到我們指定的redirect_uri,同時攜帶一個參數code

https://www.baidu.com/?code=gCWpZE

②授權碼模式獲取access_token等令牌信息

/oauth/token

訪問地址:http://localhost:8080/oauth/token
1.Basic認證:username:clientapp password:112233
2.head設置Content-Type:application/x-www-form-urlencoded
3.body傳遞參數:
code:剛得到的code
grant_type:authorization_code 授權碼模式
redirect_uri:www.baidu.com 重定向地址
scope:server

得到的返回值:

{
"access_token": "cc978538-af44-4d85-be04-743a9ad95dea",
"token_type": "bearer",
"refresh_token": "47e884f6-f38b-45f5-91f9-0ca2be05e0f2",
"expires_in": 3599,
"scope": "server"
}

③刷新令牌模式獲取access_token等令牌信息

/oauth/token

路徑與授權碼模式相同,需要將body傳遞的參數修改爲:

refresh_token:授權碼模式返回的刷新令牌,可以用來重新獲得access_token
grant_type:refresh_token 刷新令牌模式
redirect_uri:www.baidu.com
scope:server

返回值與授權碼模式相同
④資源服務器與授權服務器通信接口

/oauth/check_token

因爲這個demo我們做了資源服務器與授權服務器分離處理,因此資源服務器在收到Authorization請求頭中拼裝的access_token時,需要與授權服務器通信來驗證access_token信息的正確性。

七、資源服務器

資源服務器的登錄功能不再重複贅述,這裏貼出訪問受保護資源有關的配置文件,之後我們拆解來說:

@Configuration
@EnableResourceServer
class OAuth2ResourceServer extends ResourceServerConfigurerAdapter {


    @Value("${client-id}")
    private String clientId;

    @Value("${client-secret}")
    private String clientSecret;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 對 "/api/**" 開啓認證
                .anyRequest()
                .authenticated()
                .and()
                .requestMatchers()
                .antMatchers("/resource/**");
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.tokenServices(tokenServices());//.resourceId(SPARKLR_RESOURCE_ID);
    }

    @Bean
    public ResourceServerTokenServices tokenServices() {
        RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
//這裏硬編碼客戶端信息,服務端硬編碼保存在內存裏,生產上請修改
        remoteTokenServices.setClientId(clientId);
        remoteTokenServices.setClientSecret(clientSecret);
        remoteTokenServices.setRestTemplate(restTemplate());
        remoteTokenServices.setAccessTokenConverter(accessTokenConverter());
        return remoteTokenServices;
    }

    @Bean
    public RestTemplate restTemplate(){
        RestTemplate restTemplate = new RestTemplate();
        List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters();
        for (HttpMessageConverter<?> converter : converters) {
            if (converter instanceof MappingJackson2HttpMessageConverter) {
                MappingJackson2HttpMessageConverter jsonConverter = (MappingJackson2HttpMessageConverter) converter;
                jsonConverter.setObjectMapper(new ObjectMapper());
                List<MediaType> list = new ArrayList<MediaType>();
                list.add(new MediaType("application", "json", MappingJackson2HttpMessageConverter.DEFAULT_CHARSET));
                list.add(new MediaType("text", "javascript", MappingJackson2HttpMessageConverter.DEFAULT_CHARSET));
                jsonConverter.setSupportedMediaTypes(list);
            }
        }
        return restTemplate;
    }

    @Bean
    public AccessTokenConverter accessTokenConverter() {
        return new DefaultAccessTokenConverter();
    }

}

1.受保護資源有關配置

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 對 "/api/**" 開啓認證
                .anyRequest()
                .authenticated()
                .and()
                .requestMatchers()
                .antMatchers("/resource/**");
    }

這裏我們是配置了對以/resource開頭的訪問請求開啓權限認證,也就是說如果訪問/resource的保護資源,必須要在請求頭中添加正確拼裝且沒有失效的令牌纔可以獲取數據。

2.資源服務器與授權服務器分離配置

我們使用RemoteTokenServices來實現資源服務器與授權服務器通信


    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.tokenServices(tokenServices());//.resourceId(SPARKLR_RESOURCE_ID);
    }

    @Bean
    public ResourceServerTokenServices tokenServices() {
        RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
//這裏硬編碼客戶端信息,服務端硬編碼保存在內存裏,生產上請修改
        remoteTokenServices.setClientId(clientId);
        remoteTokenServices.setClientSecret(clientSecret);
        remoteTokenServices.setRestTemplate(restTemplate());
        remoteTokenServices.setAccessTokenConverter(accessTokenConverter());
        return remoteTokenServices;
    }

這裏我們重點看tokenServices()方法,setCheckTokenEndpointUrl指定了與授權服務器通信檢查token的url,clientId與clientSecret是資源服務器自己的id與secret,可以理解爲資源服務器也是授權服務器的一個客戶端,需要在oauth_client_details表中添加相應的數據。
RestTemplate的配置是配置了通信時的編碼信息。
AccessTokenConverter定義了使用默認的token轉換器。

八、客戶端

客戶端我們做的事情主要是通過RestTemplate來對各種訪問流程進行封裝,由於這次demo的重點不在客戶端的配置,因此相對來說客戶端比較粗糙,主要是用來實現我們的一些功能演示過程。

1.訪問授權服務器獲取令牌

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>權限認證</title>
</head>
<body>
<h1>權限認證</h1>
<button onclick="auth();">開始權限認證</button>
<script>

    function auth(){
        window.location.href = "http://localhost:8080/oauth/authorize?client_id=clientapp&redirect_uri=http://localhost:8085/portal/receive&response_type=code&scope=server";
    }
</script>
</body>
</html>

這裏我們寫了一個簡單的前端頁面,跳轉訪問授權服務器的授權接口:

http://localhost:8080/oauth/authorize?client_id=clientapp&redirect_uri=http://localhost:8085/portal/receive&response_type=code&scope=server

寫一個接收重定向請求的controller:

    @RequestMapping("receive")
    @ResponseBody
    public String receive(String code) {
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders httpHeaders = new HttpHeaders() {{
//            String auth = username + ":" + password;
            String auth = "clientapp" + ":" + "112233";
            byte[] encodedAuth = Base64.encodeBase64(
                    auth.getBytes(Charset.forName("US-ASCII")));
            String authHeader = "Basic " + new String(encodedAuth);
            set("Authorization", authHeader);
        }};
        MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<String, Object>();
        paramMap.add("code", code);
        paramMap.add("grant_type", "authorization_code");
        paramMap.add("redirect_uri", "http://localhost:8085/portal/receive");
        paramMap.add("scope", "server");

        ResponseEntity<String> responseEntity = restTemplate.exchange
                ("http://localhost:8080/oauth/token", HttpMethod.POST, new HttpEntity<MultiValueMap<String, Object>>(paramMap, httpHeaders), String.class);
        String body = responseEntity.getBody();
        System.err.println("1"+body);
        JSONObject jsonObject = JSON.parseObject(body);
        String accessToken = jsonObject.getString("access_token");
        String tokenType = jsonObject.getString("token_type");
        String refreshToken = jsonObject.getString("refresh_token");
        String expiresIn = jsonObject.getString("expires_in");
        String scope = jsonObject.getString("scope");

        OauthToken oauthToken = new OauthToken();
        IdWorker iw = new IdWorker();
        String id = String.valueOf(iw.nextId());
        oauthToken.setId(id);
        oauthToken.setAccessToken(accessToken);
        oauthToken.setTokenType(tokenType);
        oauthToken.setRefreshToken(refreshToken);
        oauthToken.setExpiresIn(expiresIn);
        oauthToken.setScope(scope);
        Date day=new Date();
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        oauthToken.setCreateDatetime(df.format(day));
        oauthToken.setDelFlag("0");
        oauthTokenMapper.insert(oauthToken);
        return "授權成功!";
    }

上面做的主要分爲兩步,第一步是通過RestTemplate拼裝相應的參數來獲取令牌信息,第二步是把令牌信息入庫持久化。

CREATE TABLE `oauth_token` (
  `id` varchar(64) CHARACTER SET utf8 NOT NULL,
  `access_token` varchar(64) CHARACTER SET utf8 DEFAULT NULL,
  `token_type` varchar(64) CHARACTER SET utf8 DEFAULT NULL,
  `refresh_token` varchar(64) CHARACTER SET utf8 DEFAULT NULL,
  `expires_in` varchar(64) CHARACTER SET utf8 DEFAULT NULL,
  `scope` varchar(64) CHARACTER SET utf8 DEFAULT NULL,
  `create_datetime` varchar(64) DEFAULT NULL,
  `del_flag` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

這裏我們搞了一個非常簡單的數據庫,實際工作中至少應該expires_in轉換爲到期時間,或者通過redis來存儲令牌信息。
拿到了令牌信息,我們的授權認證也就成功了。

2.使用access_token來訪問資源服務器


    @RequestMapping("getOrder")
    @ResponseBody
    private String getOrder(String data){
        OauthToken oauthToken = oauthTokenMapper.findRecentEntity();
        String accessToken = oauthToken.getAccessToken();
        String tokenType = oauthToken.getTokenType();
        String res = tokenType+" "+accessToken;
        HttpHeaders httpHeaders = new HttpHeaders() {{
            set("Authorization", res);
        }};
        RestTemplate restTemplate = new RestTemplate();
        MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<String, Object>();
        paramMap.add("data", data);
        ResponseEntity<String> responseEntity = restTemplate.exchange
                ("http://localhost:8081/resource/order/test", HttpMethod.POST, new HttpEntity<MultiValueMap<String, Object>>(paramMap,httpHeaders), String.class);
        String r = responseEntity.getBody();
        System.err.println(r);
        return r;
    }

可以看到我們是想訪問資源服務器的受保護資源:

http://localhost:8081/resource/order/test

我們拼裝了一個Authorization請求頭,具體內容爲tokenType空格再加accessToken

3.實現客戶端訪問指定接口完成資源服務器登錄

這裏是工作中的一個需求,聽起來比較奇怪,實際上就是推送客戶端當前的登錄用戶信息給資源服務器,資源服務器實現登錄。
客戶端Controller:


    @RequestMapping("/out/login")
    @ResponseBody
    private String outLogin(String data){
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        HttpSession session =  request.getSession();
        String username = (String)session.getAttribute("username");
        String password = (String)session.getAttribute("password");
        OauthToken oauthToken = oauthTokenMapper.findRecentEntity();
        String accessToken = oauthToken.getAccessToken();
        String tokenType = oauthToken.getTokenType();
        String res = tokenType+" "+accessToken;
        System.err.println(res);
        HttpHeaders httpHeaders = new HttpHeaders() {{
            set("Authorization", res);
        }};
        RestTemplate restTemplate = new RestTemplate();
        MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<String, Object>();
        paramMap.add("username", username);
        paramMap.add("password", password);
        ResponseEntity<String> responseEntity = restTemplate.exchange
                ("http://localhost:8081/resource/out/login", HttpMethod.POST, new HttpEntity<MultiValueMap<String, Object>>(paramMap,httpHeaders), String.class);
        String r = responseEntity.getBody();
        System.err.println(r);
        return r;
    }

多了一步session中拿當前登錄用戶信息,實際工作中這裏應該是拿用戶名和一個唯一標識,不是密碼的明文傳輸。
資源服務器的接收Controller:


    @RequestMapping("/out/login")
    public String outLogin(Model model,String username, String password) throws IOException, ServletException {
        System.err.println("aaa"+username);
        System.err.println("aaa"+password);
        model.addAttribute("username", username);
        model.addAttribute("password", password);
        return "/portal/wait";
    }

這裏我們是在/portal/wait頁面中做了一個location跳轉,跳轉到一個隱藏表單自動提交頁面,這個提交頁面的代碼如下所示:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8"/>
    <title>Title</title>
</head>
<body>
<input type="hidden" id="user" th:value="${username}"/>
<input type="hidden" id="pass" th:value="${password}"/>
<form style='display:none;' action="/login" id="fm1" method="post">

    <div class="login-mmdl">
        <div class="login-input">
            <div>
                <span>用戶名:</span><input id="username" name="username" onkeyup="keyup();" placeholder="請輸入用戶名手機號" type="text" />
            </div>
            <div>
                <span>密碼:</span><input id="password" name="password" placeholder="請輸入密碼" type="password" />
            </div>
        </div>
        <button class="login-btn-dl text-white" href="javascript:;" id="username_password" onclick="login();">&nbsp;&nbsp;</button>
    </div>
</form>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<h1 style='display:none;' id="success">登錄成功</h1>
<script type='text/javascript'>
    var username = "${username}";
    var password = "${password}";
    $("#username").val(username);
    $("#password").val(password);
    $("#fm1").submit();
    $("#success").show();

</script>
</body>
</html>

這裏我們拿到了客戶端傳過來的用戶名和密碼,將參數賦值到隱藏表單中,提交表單到資源服務器的/login接口中,資源服務器登錄成功。

注:這裏會有一個問題,資源服務器登錄後,客戶端的登錄狀態就被覆蓋掉了,這是同一個ip下cookie會共享的問題,具體原因及解決方法見我的另一篇文章:https://blog.csdn.net/a624193873/article/details/106417085

九、完整流程演示

1.客戶端登錄:

在這裏插入圖片描述

2.跳轉授權認證頁面

在這裏插入圖片描述

3.完成授權認證

在這裏插入圖片描述
在這裏插入圖片描述
上圖中登錄的是授權服務器。

在這裏插入圖片描述

4.訪問受保護資源

在這裏插入圖片描述

5.客戶端訪問指定接口完成資源服務器登錄

在這裏插入圖片描述
在這裏插入圖片描述
登錄成功,跳轉到8081資源服務器的首頁。

十、項目地址

https://github.com/KD-oauth/oauth2-demo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章