Spring Cloud实战系列(十) - 单点登陆JWT与Spring Security OAuth

相关

  1. Spring Cloud实战系列(一) - 服务注册与发现Eureka php

  2. Spring Cloud实战系列(二) - 客户端调用Rest + Ribbon html

  3. Spring Cloud实战系列(三) - 声明式客户端Feign java

  4. Spring Cloud实战系列(四) - 熔断器Hystrix mysql

  5. Spring Cloud实战系列(五) - 服务网关Zuul git

  6. Spring Cloud实战系列(六) - 分布式配置中心Spring Cloud Configgithub

  7. Spring Cloud实战系列(七) - 服务链路追踪Spring Cloud Sleuthweb

  8. Spring Cloud实战系列(八) - 微服务监控Spring Boot Admin算法

  9. Spring Cloud实战系列(九) - 服务认证受权Spring Cloud OAuth 2.0 spring

  10. Spring Cloud实战系列(十) - 单点登陆JWT与Spring Security OAuth sql

前言

经过 JWT 配合 Spring Security OAuth2 使用的方式,能够避免 每次请求远程调度 认证受权服务。资源服务器 只须要从 受权服务器 验证一次,返回 JWT。返回的 JWT 包含了 用户 的全部信息,包括 权限信息

正文

1. 什么是JWT

JSON Web TokenJWT)是一种开放的标准(RFC 7519),JWT 定义了一种 紧凑自包含 的标准,旨在将各个主体的信息包装为 JSON 对象。主体信息 是经过 数字签名 进行 加密验证 的。常用 HMAC 算法或 RSA公钥/私钥非对称性加密)算法对 JWT 进行签名,安全性很高

  • 紧凑型数据体积小,可经过 POST 请求参数HTTP 请求头 发送。

  • 自包含JWT 包含了主体的全部信息,避免了 每一个请求 都须要向 Uaa 服务验证身份,下降了 服务器的负载

2. JWT的结构

JWT 的结构由三部分组成:Header(头)、Payload(有效负荷)和 Signature(签名)。所以 JWT 一般的格式是 xxxxx.yyyyy.zzzzz

2.1. Header

Header 一般是由 两部分 组成:令牌的 类型(即 JWT)和使用的 算法类型,如 HMACSHA256RSA。例如:

{
    "typ": "JWT",
    "alg": "HS256"
}
复制代码

HeaderBase64 编码做为 JWT第一部分,不建议在 JWTHeader 中放置 敏感信息

2.2. Payload

第二部分 PayloadJWT主体内容部分,它包含 声明 信息。声明是关于 用户其余数据 的声明。

声明有三种类型: registeredpublicprivate

  • Registered claimsJWT 提供了一组 预约义 的声明,它们不是 强制的,可是推荐使用。JWT 指定 七个默认 字段供选择:
注册声明 字段含义
iss 发行人
exp 到期时间
sub 主题
aud 用户
nbf 在此以前不可用
iat 发布时间
jti 用于标识JWT的ID
  • Public claims:能够随意定义。

  • Private claims:用于在 赞成使用 它们的各方之间 共享信息,而且不是 注册的公开的 声明。

下面是 Payload 部分的一个示例:

{
    "sub": "123456789",
    "name": "John Doe",
    "admin": true
}
复制代码

PayloadBase64 编码做为 JWT第二部分,不建议在 JWTPayload 中放置 敏感信息

2.3. Signature

要建立签名部分,须要利用 秘钥Base64 编码后的 HeaderPayload 进行 加密,加密算法的公式以下:

HMACSHA256(
    base64UrlEncode(header) + '.' +
    base64UrlEncode(payload),
    secret
)
复制代码

签名 能够用于验证 消息传递过程 中有没有被更改。对于使用 私钥签名token,它还能够验证 JWT发送方 是否为它所称的 发送方

3. JWT的工做方式

客户端 获取 JWT 后,对于之后的 每次请求,都不须要再经过 受权服务 来判断该请求的 用户 以及该 用户的权限。在微服务系统中,能够利用 JWT 实现 单点登陆。认证流程图以下:

4. 案例工程结构

  • eureka-server:做为 注册服务中心,端口号为 8761。这里再也不演示搭建。

  • auth-service:做为 受权服务受权 须要用户提供 客户端client IdClient Secret,以及 受权用户usernamepassword。这些信息 准备无误 以后,auth-service 会返回 JWT,该 JWT 包含了用户的 基本信息权限点信息,并经过 RSA 私钥 进行加密。

  • user-service:做为 资源服务,它的 资源 被保护起来,须要相应的 权限 才能访问。user-service 服务获得 用户请求JWT 后,先经过 公钥 解密 JWT,获得 JWT 对应的 用户信息用户权限信息,再经过 Spring Security 判断该用户是否有 权限 访问该资源。

工程原理示意图以下:

5. 构建auth-service受权服务

  • 新建一个 auth-service 项目模块,完整的 pom.xml 文件配置以下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>io.github.ostenant.springcloud</groupId>
    <artifactId>auth-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>auth-service</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.SR1</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!--防止jks文件被mavne编译致使不可用-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <configuration>
                    <nonFilteredFileExtensions>
                        <nonFilteredFileExtension>cert</nonFilteredFileExtension>
                        <nonFilteredFileExtension>jks</nonFilteredFileExtension>
                    </nonFilteredFileExtensions>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
复制代码
  • 修改 auth-service 的配置文件 application.yml 文件以下:
spring:
 application:
 name: auth-service
 datasource:
 driver-class-name: com.mysql.jdbc.Driver
 url: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
 username: root
 password: 123456
 jpa:
 hibernate:
 ddl-auto: update
 show-sql: true
server:
 port: 9999
eureka:
 client:
 serviceUrl:
 defaultZone: http://localhost:8761/eureka/
复制代码
  • auth-service 配置 Spring Security 安全登陆管理,用于保护 token 发放验证 的资源接口。
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserServiceDetail userServiceDetail;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable() //关闭CSRF
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
            .and()
                .authorizeRequests()
                .antMatchers("/**").authenticated()
            .and()
                .httpBasic();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userServiceDetail).passwordEncoder(new BCryptPasswordEncoder());
    }
}
复制代码

UserServiceDetail.java

@Service
public class UserServiceDetail implements UserDetailsService {
    @Autowired
    private UserDao userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByUsername(username);
    }
}
复制代码

UserRepository.java

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}
复制代码

实体类 User 和上一篇文章的内容同样,须要实现 UserDetails 接口,实体类 Role 须要实现 GrantedAuthority 接口。

User.java

@Entity
public class User implements UserDetails, Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false,  unique = true)
    private String username;

    @Column
    private String password;

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
    private List<Role> authorities;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    public void setAuthorities(List<Role> authorities) {
        this.authorities = authorities;
    }

    @Override
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

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

    public void setPassword(String password) {
        this.password = password;
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return true;
    }
}
复制代码

Role.java

@Entity
public class Role implements GrantedAuthority {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public String getAuthority() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }
}
复制代码
  • 新建一个配置类 OAuth2Config,为 auth-service 配置 认证服务,代码以下:
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 将客户端的信息存储在内存中
        clients.inMemory()
                // 配置一个客户端
                .withClient("user-service")
                .secret("123456")
                // 配置客户端的域
                .scopes("service")
                 // 配置验证类型为refresh_token和password
                .authorizedGrantTypes("refresh_token", "password")
                // 配置token的过时时间为1h
                .accessTokenValiditySeconds(3600 * 1000);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 配置token的存储方式为JwtTokenStore
        endpoints.tokenStore(tokenStore())
                 // 配置用于JWT私钥加密的加强器
                 .tokenEnhancer(jwtTokenEnhancer())
                 // 配置安全认证管理
                 .authenticationManager(authenticationManager);
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtTokenEnhancer());
    }

    @Bean
    protected JwtAccessTokenConverter jwtTokenEnhancer() {
        // 配置jks文件
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("fzp-jwt.jks"), "fzp123".toCharArray());
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("fzp-jwt"));
        return converter;
    }
}
复制代码
  • 生成用于 Token 加密的 私钥文件 fzp-jwt.jks

jks 文件的生成须要使用 Java keytool 工具,保证 Java 环境变量没问题,输入命令以下:

$ keytool -genkeypair -alias fzp-jwt 
          -validity 3650 
          -keyalg RSA 
          -dname "CN=jwt,OU=jtw,O=jwt,L=zurich,S=zurich, C=CH" 
          -keypass fzp123 
          -keystore fzp-jwt.jks 
          -storepass fzp123
复制代码

其中,-alias 选项为 别名-keyalg加密算法-keypass-storepass密码选项-keystorejks文件名称-validity 为配置 jks 文件 过时时间(单位:天)。

生成的 jks 文件做为 私钥,只容许 受权服务 所持有,用做 加密生成 JWT。把生成的 jks 文件放到 auth-service 模块的 src/main/resource 目录下便可。

  • 生成用于 JWT 解密的 公钥

对于 user-service 这样的 资源服务,须要使用 jks公钥JWT 进行 解密。获取 jks 文件的 公钥 的命令以下:

$ keytool -list -rfc 
          --keystore fzp-jwt.jks | openssl x509 
          -inform pem 
          -pubkey
复制代码

这个命令要求安装 openSSL 下载地址,而后手动把安装的 openssl.exe 所在目录配置到 环境变量

输入密码 fzp123 后,显示的信息不少,只须要提取 PUBLIC KEY,即以下所示:

-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlCFiWbZXIb5kwEaHjW+/ 7J4b+KzXZffRl5RJ9rAMgfRXHqGG8RM2Dlf95JwTXzerY6igUq7FVgFjnPbexVt3 vKKyjdy2gBuOaXqaYJEZSfuKCNN/WbOF8e7ny4fLMFilbhpzoqkSHiR+nAHLkYct OnOKMPK1SwmvkNMn3aTEJHhxGh1RlWbMAAQ+QLI2D7zCzQ7Uh3F+Kw0pd2gBYd8W +DKTn1Tprugdykirr6u0p66yK5f1T9O+LEaJa8FjtLF66siBdGRaNYMExNi21lJk i5dD3ViVBIVKi9ZaTsK9Sxa3dOX1aE5Zd5A9cPsBIZ12spYgemfj6DjOw6lk7jkG 9QIDAQAB -----END PUBLIC KEY-----

新建一个 public.cert 文件,将上面的 公钥信息 复制到 public.cert 文件中并保存。并将文件放到 user-service资源服务src/main/resources 目录下。至此 auth-service 搭建完毕。

  • pom.xml 中配置 jks 文件后缀过滤器

maven 在项目编译时,可能会将 jks 文件 编译,致使 jks 文件 乱码,最后不可用。须要在 pom.xml 文件中添加如下内容:

<!-- 防止jks文件被maven编译致使不可用 -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-resources-plugin</artifactId>
    <configuration>
        <nonFilteredFileExtensions>
            <nonFilteredFileExtension>cert</nonFilteredFileExtension>
            <nonFilteredFileExtension>jks</nonFilteredFileExtension>
        </nonFilteredFileExtensions>
    </configuration>
</plugin>
复制代码
  • 最后在启动类上配置 @EnableEurekaClient 注解开启服务注册功能。
@EnableEurekaClient
@SpringBootApplication
public class AuthServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthServiceApplication.class, args);
    }
}
复制代码

6. 构建user-service资源服务

  • 新建一个 user-service 项目模块,完整的 pom.xml 文件配置以下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>io.github.ostenant.springcloud</groupId>
    <artifactId>user-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>user-service</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.SR1</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
复制代码
  • 修改 user-service 的配置文件 application.yml,配置 应用名称user-service端口号9090。另外,须要配置 feign.hystrix.enabletrue,即开启 FeignHystrix 功能。完整的配置代码以下:
server:
 port: 9090
eureka:
 client:
 service-url:
 defaultZone: http://localhost:8761/eureka/
spring:
 application:
 name: user-service
 datasource:
 driver-class-name: com.mysql.jdbc.Driver
 url: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
 username: root
 password: 123456
 jpa:
 hibernate:
 ddl-auto: update
 show-sql: true
feign:
 hystrix:
 enabled: true
复制代码
  • 配置 资源服务

注入 JwtTokenStore 类型的 Bean,同时初始化 JWT 转换器 JwtAccessTokenConverter,设置用于解密 JWT公钥

@Configuration
public class JwtConfig {
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Bean
    @Qualifier("tokenStore")
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    @Bean
    public JwtAccessTokenConverter jwtTokenEnhancer() {
        // 用做JWT转换器
        JwtAccessTokenConverter converter =  new JwtAccessTokenConverter();
        Resource resource = new ClassPathResource("public.cert");
        String publicKey;
        try {
            publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        //设置公钥
        converter.setVerifierKey(publicKey);
        return converter;
    }
}
复制代码

配置 资源服务 的认证管理,除了 注册登陆 的接口以外,其余的接口都须要 认证

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter{
    @Autowired
    private TokenStore tokenStore;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/user/login","/user/register").permitAll()
            .antMatchers("/**").authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(tokenStore);
    }
}
复制代码

新建一个配置类 GlobalMethodSecurityConfig,经过 @EnableGlobalMethodSecurity 注解开启 方法级别安全验证

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class GlobalMethodSecurityConfig {
}
复制代码
  • 实现用户注册接口

拷贝 auth-service 模块的 UserRoleUserRepository 三个类到本模块。在 Service 层的 UserService 编写一个 插入用户 的方法,代码以下:

@Service
public class UserServiceDetail {
    @Autowired
    private UserRepository userRepository;

    public User insertUser(String username,String password){
        User user=new User();
        user.setUsername(username);
        user.setPassword(BPwdEncoderUtil.BCryptPassword(password));
        return userRepository.save(user);
    }
}
复制代码

配置用于用户密码 加密 的工具类 BPwdEncoderUtil

public class BPwdEncoderUtil {
    private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

    public static String BCryptPassword(String password){
        return encoder.encode(password);
    }

    public static boolean matches(CharSequence rawPassword, String encodedPassword){
        return encoder.matches(rawPassword,encodedPassword);
    }
}
复制代码

实现一个 用户注册API 接口 /user/register,代码以下:

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    UserServiceDetail userServiceDetail;

    @PostMapping("/register")
    public User postUser(@RequestParam("username") String username, @RequestParam("password") String password){
       return userServiceDetail.insertUser(username, password);
    }
}
复制代码
  • 实现用户登陆接口

Service 层的 UserServiceDetail 中添加一个 login() 方法,代码以下:

@Service
public class UserServiceDetail {

    @Autowired
    private AuthServiceClient client;

    public UserLoginDTO login(String username, String password) {
        // 查询数据库
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UserLoginException("error username");
        }

        if(!BPwdEncoderUtil.matches(password,user.getPassword())){
            throw new UserLoginException("error password");
        }

        // 从auth-service获取JWT
        JWT jwt = client.getToken("Basic dXNlci1zZXJ2aWNlOjEyMzQ1Ng==", "password", username, password);
        if(jwt == null){
            throw new UserLoginException("error internal");
        }

        UserLoginDTO userLoginDTO=new UserLoginDTO();
        userLoginDTO.setJwt(jwt);
        userLoginDTO.setUser(user);
        return userLoginDTO;
    }
}
复制代码

AuthServiceClient 做为 Feign Client,经过向 auth-service 服务接口 /oauth/token 远程调用获取 JWT。在请求 /oauth/tokenAPI 接口中,须要在 请求头 传入 Authorization 信息,认证类型 ( grant_type )、用户名 ( username ) 和 密码 ( password ),代码以下:

@FeignClient(value = "auth-service", fallback = AuthServiceHystrix.class)
public interface AuthServiceClient {
    @PostMapping("/oauth/token")
    JWT getToken(@RequestHeader("Authorization") String authorization, @RequestParam("grant_type") String type, @RequestParam("username") String username, @RequestParam("password") String password);
}
复制代码

其中,AuthServiceHystrixAuthServiceClient熔断器,代码以下:

@Component
public class AuthServiceHystrix implements AuthServiceClient {
    private static final Logger LOGGER = LoggerFactory.getLogger(AuthServiceHystrix.class);

    @Override
    public JWT getToken(String authorization, String type, String username, String password) {
        LOGGER.warn("Fallback of getToken is executed")
        return null;
    }
}
复制代码

JWT 包含了 access_tokentoken_typerefresh_token 等信息,代码以下:

public class JWT {
    private String access_token;
    private String token_type;
    private String refresh_token;
    private int expires_in;
    private String scope;
    private String jti;

    public String getAccess_token() {
        return access_token;
    }

    public void setAccess_token(String access_token) {
        this.access_token = access_token;
    }

    public String getToken_type() {
        return token_type;
    }

    public void setToken_type(String token_type) {
        this.token_type = token_type;
    }

    public String getRefresh_token() {
        return refresh_token;
    }

    public void setRefresh_token(String refresh_token) {
        this.refresh_token = refresh_token;
    }

    public int getExpires_in() {
        return expires_in;
    }

    public void setExpires_in(int expires_in) {
        this.expires_in = expires_in;
    }

    public String getScope() {
        return scope;
    }

    public void setScope(String scope) {
        this.scope = scope;
    }

    public String getJti() {
        return jti;
    }

    public void setJti(String jti) {
        this.jti = jti;
    }
}
复制代码

UserLoginDTO 包含了一个 User 和一个 JWT 成员属性,用于返回数据的实体:

public class UserLoginDTO {
    private JWT jwt;
    private User user;

    public JWT getJwt() {
        return jwt;
    }

    public void setJwt(JWT jwt) {
        this.jwt = jwt;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }
}
复制代码

登陆异常类 UserLoginException

public class UserLoginException extends RuntimeException {
    public UserLoginException(String message) {
        super(message);
    }
}
复制代码

全局异常处理 切面类 ExceptionHandle

@ControllerAdvice
@ResponseBody
public class ExceptionHandler {
    @ExceptionHandler(UserLoginException.class)
    public ResponseEntity<String> handleException(Exception e) {
        return new ResponseEntity(e.getMessage(), HttpStatus.OK);
    }
}
复制代码

Web 层的 UserController 类中新增一个登陆的 API 接口 /user/login 以下:

@PostMapping("/login")
public UserLoginDTO login(@RequestParam("username") String username, @RequestParam("password") String password) {
    return userServiceDetail.login(username,password);
}
复制代码
  • 为了测试 用户权限,再新增一个 /foo 接口,该接口须要 ROLE_ADMIN 权限才能正常访问。
@RequestMapping(value = "/foo", method = RequestMethod.GET)
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String getFoo() {
    return "i'm foo, " + UUID.randomUUID().toString();
}
复制代码
  • 最后在应用的启动类上使用注解 @EnableFeignClients 开启 Feign 的功能便可。
@SpringBootApplication
@EnableFeignClients
@EnableEurekaClient
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}
复制代码

依次启动 eureka-serviceauth-serviceuser-service 三个服务。

7. 使用Postman测试

  • 注册一个用户,返回注册成功信息

  • 使用用户名密码登陆获取 JWT

  • 复制上面的 access_tokenheader 头部,请求须要 用户权限/user/foo 接口
"Authorization": "Bearer {access_token}"
复制代码

由于没有权限,访问被拒绝。在数据库手动添加 ROLE_ADMIN 权限,并与该用户关联。从新登陆并获取 JWT,再次请求 /user/foo 接口。

总结

在本案例中,用户经过 登陆接口 来获取 受权服务 加密后的 JWT。用户成功获取 JWT 后,在之后每次访问 资源服务 的请求中,都须要携带上 JWT资源服务 经过 公钥解密 JWT解密成功 后能够获取 用户信息权限信息,从而判断该 JWT 所对应的 用户 是谁,具备什么 权限

  • 优势

获取一次 Token,屡次使用,资源服务 再也不每次访问 受权服务Token 所对应的 用户信息 和用户的 权限信息

  • 缺点

一旦 用户信息 或者 权限信息 发生了改变,Token 中存储的相关信息并 没有改变,须要 从新登陆 获取新的 Token。就算从新获取了 Token,若是原来的 Token 没有过时,仍然是可使用的。一种改进方式是在登陆成功后,将获取的 Token 缓存网关上。若是用户的 权限更改,将 网关 上缓存的 Token 删除。当请求通过 网关,判断请求的 Token缓存 中是否存在,若是缓存中不存在该 Token,则提示用户 从新登陆

参考

  • 方志朋《深刻理解Spring Cloud与微服务构建》

欢迎关注技术公众号: 零壹技术栈

零壹技术栈

本账号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。

相关文章
相关标签/搜索