天天看點

Spring Security OAuth2搭建認證授權中心完整詳細示例

認證授權中心

添加依賴

<!-- spring web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- spring data redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
        <!-- hutool -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- spring cloud security -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-core</artifactId>
        </dependency>
        <!-- spring cloud oauth2 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        
      <dependencyManagement>
        <dependencies>
            <!-- spring cloud 依賴 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR8</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
複制代碼           

application.yml配置

server:
  port: 8888 # 端口

spring:
  application:
    name: oauth2-server # 應用名
  # 資料庫
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://127.0.0.1:3306/demo?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
  # Redis
  redis:
    port: 6379
    host: 127.0.0.1
    timeout: 3000
    database: 1
    password:

# Oauth2
client:
  oauth2:
    client-id: appId # 用戶端辨別 ID
    secret: 123456 # 用戶端安全碼
    # 授權類型
    grant_types:
      - password
      - refresh_token
    # token 有效時間,機關秒
    token-validity-time: 2592000
    refresh-token-validity-time: 2592000
    # 用戶端通路範圍
    scopes:
      - api
      - all

# Mybatis
mybatis:
  configuration:
    map-underscore-to-camel-case: true # 開啟駝峰映射
複制代碼           

Security配置

配置使用Redis存儲Token資訊  

配置密碼的加密、解密、校驗邏輯 
 
初始化認證管理對象   

配置請求通路的放行和認證規則
複制代碼           
import cn.hutool.crypto.digest.DigestUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
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.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import javax.annotation.Resource;

/**
 * Security配置
 */
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    /**
     * 注入Redis連接配接工廠
     */
    @Resource
    private RedisConnectionFactory redisConnectionFactory;

    /**
     * 初始化RedisTokenStore,用于将token存儲至Redis
     *
     * @return
     */
    @Bean
    public RedisTokenStore redisTokenStore() {
        RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
        // 設定key的層級字首
        redisTokenStore.setPrefix("TOKEN:");
        return redisTokenStore;
    }

    /**
     * 初始化密碼編碼器,指定編碼與校驗規則,用MD5加密密碼
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        // Security官方推薦的BCryptPasswordEncoder加密與校驗類
        // 密鑰的疊代次數(預設為10)
        //return new BCryptPasswordEncoder(10);

        return new PasswordEncoder() {
            /**
             * 加密
             * @param rawPassword 原始密碼
             * @return
             */
            @Override
            public String encode(CharSequence rawPassword) {
                return DigestUtil.md5Hex(rawPassword.toString());
            }

            /**
             * 校驗密碼
             * @param rawPassword       原始密碼
             * @param encodedPassword   加密密碼
             * @return
             */
            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return DigestUtil.md5Hex(rawPassword.toString()).equals(encodedPassword);
            }
        };
    }

    /**
     * 初始化認證管理對象
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 放行和認證規則
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用csrf
        http.csrf().disable()
                .authorizeRequests()
                // 放行的請求
                .antMatchers("/oauth/**", "/actuator/**").permitAll()
                .and()
                .authorizeRequests()
                // 其他請求必須認證才能通路
                .anyRequest().authenticated();
    }
}
複制代碼           

登入認證配置

建立UserService類實作UserDetailsService類重寫loadUserByUsername方法,該方法主要實作登入、認證校驗邏輯,這裡簡單模拟。

public interface UserMapper {

    /**
     * 根據使用者名 or 手機号 or 郵箱查詢使用者資訊
     * @param account
     * @return
     */
    @Select("select id, username, phone, email, password, roles from user where " +
            "(username = #{account} or phone = #{account} or email = #{account})")
    Diners selectByAccountInfo(@Param("account") String account);

}
複制代碼           
@Service
public class UserService implements UserDetailsService {

    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StrUtil.hasBlank(username)) {
            throw new RuntimeException("使用者名不可為空");
        }
        User user= userMapper.selectByAccountInfo(username);
        if (user == null) {
            throw new UsernameNotFoundException("使用者名或密碼錯誤,請重新輸入");
        }
        return new User(username, user.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList(diners.getRoles()));
    }

}
複制代碼           

Oauth2參數配置類

讀取application.yaml檔案中的Oauth2配置資訊,并封裝到ClientOAuth2DataConfiguration類

package com.example.demo.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 用戶端配置類
 */
@Component
@ConfigurationProperties(prefix = "client.oauth2")
@Data
public class ClientOAuth2DataConfiguration {

    /**
     * 用戶端辨別ID
     */
    private String clientId;

    /**
     * 用戶端安全碼
     */
    private String secret;

    /**
     * 授權類型
     */
    private String[] grantTypes;

    /**
     * token有效期
     */
    private int tokenValidityTime;

    /**
     * refresh-token有效期
     */
    private int refreshTokenValidityTime;

    /**
     * 用戶端通路範圍
     */
    private String[] scopes;
}
複制代碼           

授權服務配置

package com.example.demo.config;

import com.example.demo.service.UserService;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import javax.annotation.Resource;

/**
 * 授權服務配置
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    /**
     * RedisTokenSore
     */
    @Resource
    private RedisTokenStore redisTokenStore;

    /**
     * 認證管理對象
     */
    @Resource
    private AuthenticationManager authenticationManager;

    /**
     * 密碼編碼器
     */
    @Resource
    private PasswordEncoder passwordEncoder;

    /**
     * 用戶端配置類
     */
    @Resource
    private ClientOAuth2DataConfiguration clientOAuth2DataConfiguration;

    /**
     * 登入校驗
     */
    @Resource
    private UserService userService;


    /**
     * 配置令牌端點安全限制
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 允許通路token的公鑰,預設/oauth/token_key是受保護的
        security.tokenKeyAccess("permitAll()")
                // 允許檢查token的狀态,預設/oauth/check_token是受保護的
                .checkTokenAccess("permitAll()");
    }

    /**
     * 用戶端配置 - 授權模型
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().withClient(clientOAuth2DataConfiguration.getClientId()) // 用戶端辨別 ID
                .secret(passwordEncoder.encode(clientOAuth2DataConfiguration.getSecret())) // 用戶端安全碼
                .authorizedGrantTypes(clientOAuth2DataConfiguration.getGrantTypes()) // 授權類型
                .accessTokenValiditySeconds(clientOAuth2DataConfiguration.getTokenValidityTime()) // token 有效期
                .refreshTokenValiditySeconds(clientOAuth2DataConfiguration.getRefreshTokenValidityTime()) // 重新整理 token 的有效期
                .scopes(clientOAuth2DataConfiguration.getScopes()); // 用戶端通路範圍
    }

    /**
     * 配置授權以及令牌的通路端點和令牌服務
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 認證器
        endpoints.authenticationManager(authenticationManager)
                // 具體登入的方法
                .userDetailsService(userService)
                // token 存儲的方式:Redis
                .tokenStore(redisTokenStore);
    }

}
複制代碼           

執行測試

請求 localhost:8888/oauth/token

參數設定

Spring Security OAuth2搭建認證授權中心完整詳細示例
Spring Security OAuth2搭建認證授權中心完整詳細示例

執行請求

Spring Security OAuth2搭建認證授權中心完整詳細示例

檢視Redis

Spring Security OAuth2搭建認證授權中心完整詳細示例

增強令牌

增強令牌就是豐富、自定義令牌包含的資訊,這部分資訊是用戶端能直接看到的

重構端點

重構/oauth/token端點

/**
 * Oauth2控制器
 */
@RestController
@RequestMapping("oauth")
public class OAuthController {

    @Resource
    private TokenEndpoint tokenEndpoint;

    @Resource
    private HttpServletRequest request;

    /**
     * 自定義Token傳回對象
     *
     * @param principal
     * @param parameters
     * @return
     * @throws HttpRequestMethodNotSupportedException
     */
    @PostMapping("token")
    public HashMap<String, Object> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        OAuth2AccessToken auth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
        DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) auth2AccessToken;
        Map<String, Object> data = new LinkedHashMap(token.getAdditionalInformation());
        data.put("accessToken", token.getValue());
        data.put("expireIn", token.getExpiresIn());
        data.put("scopes", token.getScope());
        if (token.getRefreshToken() != null) {
            data.put("refreshToken", token.getRefreshToken().getValue());
        }
        data.put("path", request.getServletPath());
        return BaseUtil.back(1, data);
    }
}
複制代碼           

執行測試

Spring Security OAuth2搭建認證授權中心完整詳細示例

重構令牌

建立SignInIdentity登入認證對象類實作UserDetails

package com.example.demo.model;

import cn.hutool.core.util.StrUtil;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 登入認證對象
 */
@Getter
@Setter
public class SignInIdentity implements UserDetails {

    /**
     * 主鍵
     */
    private Integer id;
    /**
     * 使用者名
     */
    private String username;
    /**
     * 昵稱
     */
    private String nickname;
    /**
     * 密碼
     */
    private String password;
    /**
     * 手機号
     */
    private String phone;
    /**
     * 郵箱
     */
    private String email;
    /**
     * 頭像
     */
    private String avatarUrl;
    /**
     * 角色
     */
    private String roles;
    /**
     * 是否有效 0=無效 1=有效
     */
    private int isValid;
    /**
     * 角色集合, 不能為空
     */
    private List<GrantedAuthority> authorities;

    /**
     * 擷取角色資訊
     *
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (StrUtil.isNotBlank(this.roles)) {
            String[] strings = this.roles.split(",");
            // 擷取資料庫中的角色資訊
            this.authorities = Stream.of(strings).map(role -> {
                return new SimpleGrantedAuthority(role);
            }).collect(Collectors.toList());
        } else {
            // 如果角色為空則設定為ROLE_USER
            this.authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
        }
        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return this.isValid != 0;
    }

}
複制代碼           

修改登入認證

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StrUtil.hasBlank(username)) {
            throw new RuntimeException("使用者名不可為空");
        }
        Diners diners = dinersMapper.selectByAccountInfo(username);
        if (diners == null) {
            throw new UsernameNotFoundException("使用者名或密碼錯誤,請重新輸入");
        }
        // 初始化登入認證對象
        SignInIdentity signInIdentity = new SignInIdentity();
        // 拷貝屬性
        BeanUtils.copyProperties(diners, signInIdentity);
        return signInIdentity;
        // return new User(username, diners.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList(diners.getRoles()));
    }
複制代碼           

令牌增強

/**
     * 配置授權以及令牌的通路端點和令牌服務
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 認證器
        endpoints.authenticationManager(authenticationManager)
                // 具體登入的方法
                .userDetailsService(userService)
                // token 存儲的方式:Redis
                .tokenStore(redisTokenStore)
                // 令牌增強對象,增強傳回的結果
                .tokenEnhancer((accessToken, authentication) -> {
                    // 擷取登入使用者的資訊,然後設定
                    SignInIdentity signInIdentity = (SignInIdentity) authentication.getPrincipal();
                    LinkedHashMap<String, Object> map = new LinkedHashMap<>();
                    map.put("nickname", signInIdentity.getNickname());
                    map.put("avatarUrl", signInIdentity.getAvatarUrl());
                    DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
                    token.setAdditionalInformation(map);
                    return token;
                });
    }
複制代碼           

執行測試

請求 localhost:8888/oauth/token

Spring Security OAuth2搭建認證授權中心完整詳細示例

資源服務中心

登入成功,得到token,通過token擷取資源

認證異常配置

建立MyAuthenticationEntryPoint類,處理認證失敗出現異常時的處理邏輯。

package com.example.demo.config;

import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.example.demo.utils.BaseUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;

/**
 * 認證失敗處理
 */
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Resource
    private ObjectMapper objectMapper;

    /**
     * 認證失敗處理邏輯
     *
     * @param request
     * @param response
     * @param authException
     * @throws IOException
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        // 傳回 JSON
        response.setContentType("application/json;charset=utf-8");
        // 狀态碼 401
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        // 寫出
        PrintWriter out = response.getWriter();
        String errorMessage = authException.getMessage();
        if (StrUtil.isBlank(errorMessage)) {
            errorMessage = "登入失效!";
        }
        HashMap<String, Object> result = BaseUtil.back(0, errorMessage, errorMessage);
        // ResultInfo result = ResultInfoUtil.buildError(ApiConstant.ERROR_CODE, errorMessage, request.getRequestURI());
        out.write(objectMapper.writeValueAsString(result));
        out.flush();
        out.close();
    }

}
複制代碼           

建立資源服務

package com.example.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;

import javax.annotation.Resource;

/**
 * 資源服務
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Resource
    private MyAuthenticationEntryPoint authenticationEntryPoint;

    /**
     * 配置放行的資源
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //所有請求必須認證通過
        http.authorizeRequests()
                //其他位址需要認證授權;
                .anyRequest()
                .authenticated()
                .and()
                //下邊的路徑放行
                .requestMatchers()
                .antMatchers("/user/**");
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.authenticationEntryPoint(authenticationEntryPoint);
    }

}
複制代碼           

提供資源

package com.example.demo.controller;

import com.example.demo.model.SignInIdentity;
import com.example.demo.utils.BaseUtil;
import io.micrometer.core.instrument.util.StringUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;

/**
 * 使用者中心
 */
@RestController
public class UserController {

    @Resource
    private RedisTokenStore redisTokenStore;

    /**
     * 擷取登入使用者的資訊
     *
     * @param authentication
     * @return
     */
    @GetMapping("user/getLoginUser")
    public HashMap<String, Object> getCurrentUser(Authentication authentication) {
        SignInIdentity signInIdentity = (SignInIdentity) authentication.getPrincipal();
        HashMap<String, Object> map = new HashMap<>();
        map.put("username", signInIdentity.getUsername());
        map.put("phone", signInIdentity.getPhone());
        map.put("email", signInIdentity.getEmail());
        return BaseUtil.back(1, "擷取資源成功", map);
    }

    /**
     * 安全退出
     *
     * @param access_token
     * @param authorization
     * @return
     */
    @GetMapping("user/logout")
    public HashMap<String, Object> logout(String access_token, String authorization) {
        // 判斷access_token是否為空,為空将authorization指派給access_token
        if (StringUtils.isBlank(access_token)) {
            access_token = authorization;
        }
        // 判斷authorization是否為空
        if (StringUtils.isBlank(access_token)) {
            return BaseUtil.back(1, "退出成功");
        }
        // 判斷bearer token是否為空
        if (access_token.toLowerCase().contains("bearer ".toLowerCase())) {
            access_token = access_token.toLowerCase().replace("bearer ", "");
        }
        // 清除redis token資訊
        OAuth2AccessToken oAuth2AccessToken = redisTokenStore.readAccessToken(access_token);
        if (oAuth2AccessToken != null) {
            redisTokenStore.removeAccessToken(oAuth2AccessToken);
            OAuth2RefreshToken refreshToken = oAuth2AccessToken.getRefreshToken();
            redisTokenStore.removeRefreshToken(refreshToken);
        }
        return BaseUtil.back(1, "退出成功");
    }
}

複制代碼           

執行測試

請求localhost:8888/oauth/token擷取token

{
	"code": 1,
	"message": "Successful.",	
	"data": {
		"nickname": "test",
		"avatarUrl": "/test",
		"accessToken": "2cf71a49-1f62-4e93-b27c-7cb0b4419ab4",
		"expireIn": 2588653,
		"scopes": [
			"api"
		],
		"refreshToken": "154aefe0-a0fa-43d4-91fb-c70b1e2998e4",
		"path": "/oauth/token"
	}
}
複制代碼           

使用token擷取服務資源,有兩種方式:

方式一:

請求localhost:8888/user/getLoginUser?access_token=2cf71a49-1f62-4e93-b27c-7cb0b4419ab4擷取資源

{
	"msg": "擷取資源成功",
	"code": 1,
	"data": {
		"phone": "13666666666",
		"email": null,
		"username": "test"
	}
}
複制代碼           

方式二:

請求localhost:8888/user/getLoginUser,使用Bearer auth認證

Spring Security OAuth2搭建認證授權中心完整詳細示例

擷取資源

{
	"msg": "擷取資源成功",
	"code": 1,
	"data": {
		"phone": "13666666666",
		"email": null,
		"username": "test"
	}
}
複制代碼           

校驗token

請求 localhost:8888/oauth/check_token?token=2cf71a49-1f62-4e93-b27c-7cb0b4419ab4

校驗token成功時:

{
	"avatarUrl": "/test",
	"user_name": "test",
	"scope": [
		"api"
	],
	"nickname": "test",
	"active": true,
	"exp": 1629734432,
	"authorities": [
		"ROLE_USER"
	],
	"client_id": "appId"
}
複制代碼           

校驗token失敗時:

{
	"error": "invalid_token",
	"error_description": "Token was not recognised"
}
複制代碼           

安全退出

1.請求localhost:8888/user/logout?access_token=2cf71a49-1f62-4e93-b27c-7cb0b4419ab4

2.請求localhost:8888/user/logout,使用Bearer auth認證

{
	"msg": "退出成功",
	"code": 1,
	"data": null
}
複制代碼           

退出後再次請求資源

{
	"msg": "Invalid access token: 2cf71a49-1f62-4e93-b27c-7cb0b4419ab4",
	"code": 0,
	"data": "Invalid access token: 2cf71a49-1f62-4e93-b27c-7cb0b4419ab4"
}
複制代碼           

網關校驗

添加依賴

<!-- spring cloud gateway -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- eureka client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
複制代碼           

配置application.yml

server:
  port: 9999

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 開啟配置注冊中心進行路由功能
          lower-case-service-id: true # 将服務名稱轉小寫
      routes:
        - id: oauth2-server
          uri: lb://oauth2-server
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1

# 自定義參數
secure:
  ignore:
    urls: # 配置白名單路徑
      - /actuator/**
      - /auth/oauth/**
      - /user/getLoginUser
      - /user/logout

# 配置 Eureka Server 注冊中心
eureka:
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  client:
    service-url:
      defaultZone: http://localhost:8080/eureka/        
複制代碼           

路徑白名單配置類

/**
 * 網關白名單配置
 */
@Data
@Component
@ConfigurationProperties(prefix = "secure.ignore")
public class IgnoreUrlsConfig {

    private List<String> urls;

}
複制代碼           

網關過濾器

package com.example.demo.gateway.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.example.demo.utils.BaseUtil;
import com.example.demo.config.IgnoreUrlsConfig;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;

/**
 * 網關全局過濾器
 */
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    @Resource
    private IgnoreUrlsConfig ignoreUrlsConfig;
    @Resource
    private RestTemplate restTemplate;


    /**
     * 身份校驗處理
     *
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 判斷目前的請求是否在白名單中
        AntPathMatcher pathMatcher = new AntPathMatcher();
        boolean flag = false;
        String path = exchange.getRequest().getURI().getPath();
        for (String url : ignoreUrlsConfig.getUrls()) {
            if (pathMatcher.match(url, path)) {
                flag = true;
                break;
            }
        }
        // 白名單放行
        if (flag) {
            return chain.filter(exchange);
        }
        // 擷取 access_token
        String access_token = exchange.getRequest().getQueryParams().getFirst("access_token");
        // 判斷access_token是否為空
        if (StringUtils.isBlank(access_token)) {
            return this.writeError(exchange, "請登入");
        }
        // 校驗token是否有效
        String checkTokenUrl = "http://oauth2-server/oauth/check_token?token=".concat(access_token);
        try {
            // 發送遠端請求,驗證 token
            ResponseEntity<String> entity = restTemplate.getForEntity(checkTokenUrl, String.class);
            // token無效業務邏輯處理
            if (entity.getStatusCode() != HttpStatus.OK) {
                return this.writeError(exchange, "請求失敗");
            }
            if (StringUtils.isBlank(entity.getBody())) {
                return this.writeError(exchange, "擷取token失敗");
            }
        } catch (Exception e) {
            return this.writeError(exchange, "token校驗失敗");
        }
        // 放行
        return chain.filter(exchange);
    }

    /**
     * 網關過濾器的排序,數字越小優先級越高
     *
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }

    @Resource
    private ObjectMapper objectMapper;

    public Mono<Void> writeError(ServerWebExchange exchange, String error) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        HashMap<String, Object> back = BaseUtil.back(0, error);
        String resultInfoJson;
        DataBuffer buffer = null;
        try {
            resultInfoJson = objectMapper.writeValueAsString(back);
            buffer = response.bufferFactory().wrap(resultInfoJson.getBytes(StandardCharsets.UTF_8));
        } catch (JsonProcessingException ex) {
            ex.printStackTrace();
        }
        return response.writeWith(Mono.just(buffer));
    }

}
複制代碼           

執行測試

請求localhost:9999/auth/oauth/token擷取token

{
	"code": 1,
	"message": "Successful.",	
	"data": {
		"nickname": "test",
		"avatarUrl": "/test",
		"accessToken": "7a3d7102-39eb-4d02-be4c-9a705c9db616",
		"expireIn": 2591999,
		"scopes": [
			"api"
		],
		"refreshToken": "6c974314-a13c-473a-8cdc-fb8373b3cce5",
		"path": "/oauth/token"
	}
}
複制代碼           

請求localhost:9999/auth/user/getLoginUser?access_token=7a3d7102-39eb-4d02-be4c-9a705c9db616擷取服務資源

{
	"msg": "擷取資源成功",
	"code": 1,
	"data": {
		"phone": "13666666666",
		"email": null,
		"username": "test"
	}
}
複制代碼           

請求localhost:9999/auth/user/logout?access_token=7a3d7102-39eb-4d02-be4c-9a705c9db616安全退出

{
	"msg": "退出成功",
	"code": 1,
	"data": null
}