Spring Cloud Springsecurity

Spring Cloud实战系列(十) - 单点登录JWT与Sp

2019-02-10  本文已影响211人  零壹技术栈

前言

通过 JWT 配合 Spring Security OAuth2 使用的方式,可以避免 每次请求远程调度 认证授权服务。资源服务器 只需要从 授权服务器 验证一次,返回 JWT。返回的 JWT 包含了 用户 的所有信息,包括 权限信息

正文

1. 什么是JWT

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

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

注册声明 字段含义
iss 发行人
exp 到期时间
sub 主题
aud 用户
nbf 在此之前不可用
iat 发布时间
jti 用于标识JWT的ID

下面是 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. 案例工程结构

工程原理示意图如下:

5. 构建auth-service授权服务

<?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>
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/
@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;
    }
}
@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;
    }
}

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 目录下即可。

对于 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 搭建完毕。

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
@SpringBootApplication
public class AuthServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthServiceApplication.class, args);
    }
}

6. 构建user-service资源服务

<?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>
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);
}
@RequestMapping(value = "/foo", method = RequestMethod.GET)
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String getFoo() {
    return "i'm foo, " + UUID.randomUUID().toString();
}
@SpringBootApplication
@EnableFeignClients
@EnableEurekaClient
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

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

7. 使用Postman测试

"Authorization": "Bearer {access_token}"

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

总结

在本案例中,用户通过 登录接口 来获取 授权服务 加密后的 JWT。用户成功获取 JWT 后,在以后每次访问 资源服务 的请求中,都需要携带上 JWT资源服务 通过 公钥解密 JWT解密成功 后可以获取 用户信息权限信息,从而判断该 JWT 所对应的 用户 是谁,具有什么 权限

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

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

参考


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

零壹技术栈

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

上一篇下一篇

猜你喜欢

热点阅读