前言
通过 JWT
配合 Spring Security OAuth2
使用的方式,可以避免 每次请求 都 远程调度 认证授权服务。资源服务器 只需要从 授权服务器 验证一次,返回 JWT
。返回的 JWT
包含了 用户 的所有信息,包括 权限信息。
正文
1. 什么是JWT
JSON Web Token
(JWT
)是一种开放的标准(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
)和使用的 算法类型,如 HMAC
、SHA256
和 RSA
。例如:
{
"typ": "JWT",
"alg": "HS256"
}
将 Header
用 Base64
编码作为 JWT
的 第一部分,不建议在 JWT
的 Header
中放置 敏感信息。
2.2. Payload
第二部分 Payload
是 JWT
的 主体内容部分,它包含 声明 信息。声明是关于 用户 和 其他数据 的声明。
声明有三种类型: registered
、public
和 private
。
-
Registered claims
JWT
提供了一组 预定义 的声明,它们不是 强制的,但是推荐使用。JWT
指定 七个默认 字段供选择:
注册声明 | 字段含义 |
---|---|
iss | 发行人 |
exp | 到期时间 |
sub | 主题 |
aud | 用户 |
nbf | 在此之前不可用 |
iat | 发布时间 |
jti | 用于标识JWT的ID |
Public claims:可以随意定义。
Private claims:用于在 同意使用 它们的各方之间 共享信息,并且不是 注册的 或 公开的 声明。
下面是 Payload
部分的一个示例:
{
"sub": "123456789",
"name": "John Doe",
"admin": true
}
将 Payload
用 Base64
编码作为 JWT
的 第二部分,不建议在 JWT
的 Payload
中放置 敏感信息。
2.3. Signature
要创建签名部分,需要利用 秘钥 对 Base64
编码后的 Header
和 Payload
进行 加密,加密算法的公式如下:
HMACSHA256(
base64UrlEncode(header) + '.' +
base64UrlEncode(payload),
secret
)
签名 可以用于验证 消息 在 传递过程 中有没有被更改。对于使用 私钥签名 的 token
,它还可以验证 JWT
的 发送方 是否为它所称的 发送方。
3. JWT的工作方式
客户端 获取 JWT
后,对于以后的 每次请求,都不需要再通过 授权服务 来判断该请求的 用户 以及该 用户的权限。在微服务系统中,可以利用 JWT
实现 单点登录。认证流程图如下:
4. 案例工程结构
eureka-server:作为 注册服务中心,端口号为
8761
。这里不再演示搭建。auth-service:作为 授权服务,授权 需要用户提供 客户端 的
client Id
和Client Secret
,以及 授权用户 的username
和password
。这些信息 准备无误 之后,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
为 密码选项,-keystore
为 jks
的 文件名称,-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.enable
为true
,即开启Feign
的Hystrix
功能。完整的配置代码如下:
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
模块的 User
、Role
和 UserRepository
三个类到本模块。在 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/token
的 API
接口中,需要在 请求头 传入 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);
}
其中,AuthServiceHystrix
为 AuthServiceClient
的 熔断器,代码如下:
@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_token
、token_type
和 refresh_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-service
,auth-service
和 user-service
三个服务。
7. 使用Postman测试
- 注册一个用户,返回注册成功信息
- 使用用户名密码登录获取
JWT
- 复制上面的
access_token
到header
头部,请求需要 用户权限 的/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与微服务构建》
欢迎关注技术公众号: 零壹技术栈
本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。