我们希望自己的微服务能够在用户登录之后才可以访问,而单独给每个微服务单独做用户权限模块就显得很弱了,从复用角度来说是需要重构的,从功能角度来说,也是欠缺的。尤其是前后端完全分离之后,我们的用户信息不一定存在于 Session 会话中,本节内容使用OAuth2+JWT的功能恰好能够弥补这块。
应用场景
常见的应用场景如下图,用户通过浏览器进行登录,一旦确定用户名和密码正确,那么在服务器端使用秘钥创建 JWT,并且返回给浏览器;接下来我们的请求需要在头部增加 jwt 信息,服务器端进行解密获取用户信息,然后进行其他业务逻辑处理,再返回客户端
实战案例
我们基于 Spring Cloud 的骨架进行搭建,分为3个工程,eureka 服务器,负责微服务注册;auth 服务器,负责授权,需要提供 clientId 和密码;user 微服务,一个微服务提供,他作为资源服务器,资源是被保护起来的,需要相应的权限才能访问。User 微服务得到用户请求的 JWT 之后,使用公钥解密,得到用户信息和权限信息。
编写主 maven 工程
构建一个 maven 项目,打包类型是 pom,其中该 pom 文件内容如下
4.0.0
com.cnsesan
cnsean-architecture-spring-cloud
0.0.1-SNAPSHOT
pom
org.springframework.boot
spring-boot-starter-parent
1.5.13.RELEASE
1.0.0-SNAPSHOT
1.8
io.spring.platform
platform-bom
Brussels-SR11
pom
import
org.springframework.cloud
spring-cloud-dependencies
Dalston.SR5
pom
import
org.apache.maven.plugins
maven-compiler-plugin
1.8
1.8
UTF-8
cnsesan-eureka-single
cnsesan-uaa-service
cnsesan-user-service
上述的版本是经过测试可以正常使用的,如果需要更新到 SpringBoot2.0版本,需要更新其他版本进行对应。同时也看到该 pom 内部包含3个 module,接下来我们分别来构建这3个 module。
构建 EurekaServer
这里我们构建的是单个 Eureka 服务器作为测试,真实环境是需要集群的。在父项目的基础上,右键构建,如下图(IDE 为 STS)
配置 pom,加入依赖
4.0.0
com.cnsesan
cnsean-architecture-spring-cloud
0.0.1-SNAPSHOT
cnsesan-eureka-single
org.springframework.cloud
spring-cloud-starter-eureka-server
这里仅仅引入 eureka 服务器端的依赖即可
配置 yml 文件
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://localhost:8762/eureka/
register-with-eureka: false
fetch-registry: false
# instance:
# preferIpAddress: true
server:
# 关闭自我保护模式(缺省为打开)
enable-self-preservation: false
# 续期时间,即扫描失效服务的间隔时间(缺省为60*1000ms)
eviction-interval-timer-in-ms: 5000
logging:
level:
com.netflix: INFO
server:
port: 8762
端口是8762,名称是eureka-server
在 Application 启动类中添加注解
package com.cnsesan.eureka;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class)
.web(true).run(args);
}
}
找到BootDashboard,运行eureka
构建 Uaa 授权服务
同样构建 maven 项目,导入依赖,pom 文件为
4.0.0
com.cnsesan
cnsean-architecture-spring-cloud
0.0.1-SNAPSHOT
cnsesan-uaa-service
org.springframework.cloud
spring-cloud-starter-oauth2
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-eureka
org.apache.maven.plugins
maven-resources-plugin
cert
jks
其中最后一段是防止打包的时候把公钥和私钥文件搞乱,读取不了。
接下来配置 application.yml
spring:
application:
name: uaa-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: root
jpa:
hibernate:
ddl-auto: update
show-sql: true
server:
port: 9999
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8762/eureka/
端口是9999,服务名称是 uaa-service
与 application.yml 相同地方还需要2个文件,分别是cnsesan-jwt.jks和 public.cert
我们先把这两个文件弄出来
keytool -genkeypair -alias cnsesan-jwt -validity 3650 -keyalg RSA -dname "CN=jwt,OU=cnsesan,O=cnsesan,L=zurich,S=zurich,C=CH" -keypass cnsesan123 -keystore cnsesan-jwt.jks -storepass cnsesan123
如上操作得到cnsesan-jwt.jks
然后需要的都公钥文件,如下
keytool -list -rfc --keystore cnsesan-jwt.jks | openssl x509 -inform pem -pubkey
输入密码 cnsesan123,将如下片段拷贝到新文件public.cert
可以得到public.cert
将这两个文件拷贝到 resource 目录下
接下来首先编写启动类,主要是几个注解
package com.cnsesan.uaa;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
@SpringBootApplication
@EnableResourceServer
@EnableEurekaClient
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class)
.web(true).run(args);
}
}
然后是编写我们的配置类,也是最核心的地方
首先编写配置Spring Security
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private UserServiceDetail userServiceDetail ;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.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());
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
这里有个UserServiceDetail
,实现了UserDetailsService,他的代码如下,主要是负责用户信息获取的
@Service
public class UserServiceDetail implements UserDetailsService {
@Autowired private UserDao userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username);
}
}
UserDao 类 是一个接口,使用 JPA 的方式,如下
public interface UserDao extends JpaRepository{
User findByUsername(String username);
}
User 和 Role 两个实体类需要做如下的实现
@Entity
public class User implements UserDetails, Serializable{
private static final long serialVersionUID = 1L;
@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 authorities;
public User() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setAuthorities(List authorities) {
this.authorities = authorities;
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
@Entity
public class Role implements GrantedAuthority{
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
@Column(nullable=false)
private String name;
@Override
public String getAuthority() {
return name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
其次编写OAuth2Config
,该类是配置OAuth2相关内容
@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")// 创建一个客户端 名字是user-service
.secret("123456")
.scopes("service")
.authorizedGrantTypes("refresh_token", "password")
.accessTokenValiditySeconds(3600);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtTokenEnhancer())
.authenticationManager(authenticationManager);
}
public TokenStore tokenStore() {
return new JwtTokenStore(jwtTokenEnhancer());
}
private JwtAccessTokenConverter jwtTokenEnhancer() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("cnsesan-jwt.jks"),
"cnsesan123".toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("cnsesan-jwt"));
return converter;
}
}
到此为止,授权服务器搭建完毕,启动,
在测试之前,数据库需要增加一些表
DROP TABLE IF EXISTS `clientdetails`;
CREATE TABLE `clientdetails` (
`appId` varchar(128) NOT NULL,
`resourceIds` varchar(256) DEFAULT NULL,
`appSecret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`grantTypes` varchar(256) DEFAULT NULL,
`redirectUrl` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additionalInformation` varchar(4096) DEFAULT NULL,
`autoApproveScopes` varchar(256) DEFAULT NULL,
PRIMARY KEY (`appId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;;
-- ----------------------------
-- Table structure for oauth_access_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(128) NOT NULL,
`user_name` varchar(256) DEFAULT NULL,
`client_id` varchar(256) DEFAULT NULL,
`authentication` blob,
`refresh_token` varchar(256) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;;
-- ----------------------------
-- Table structure for oauth_approvals
-- ----------------------------
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (
`userId` varchar(256) DEFAULT NULL,
`clientId` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`status` varchar(10) DEFAULT NULL,
`expiresAt` datetime DEFAULT NULL,
`lastModifiedAt` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;;
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(128) NOT NULL,
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`authorized_grant_types` varchar(256) DEFAULT NULL,
`web_server_redirect_uri` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;;
-- ----------------------------
-- Table structure for oauth_client_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(128) NOT NULL,
`user_name` varchar(256) DEFAULT NULL,
`client_id` varchar(256) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;;
-- ----------------------------
-- Table structure for oauth_code
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(256) DEFAULT NULL,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;;
-- ----------------------------
-- Table structure for oauth_refresh_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;;
SET FOREIGN_KEY_CHECKS = 1;
现在可以测试
curl user-service:123456@localhost:9999/oauth/token -d grant_type=password -d username=ts -d password=123456
得到如下
{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MzA4NTEyNjMsInVzZXJfbmFtZSI6InRzIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIiwiUk9MRV9BRE1JTiIsIkFETUlOIl0sImp0aSI6ImNkYmE1MTExLTlmNmEtNGU1NS04ZmRhLTUzYzAzOWYxOWRiMiIsImNsaWVudF9pZCI6InVzZXItc2VydmljZSIsInNjb3BlIjpbInNlcnZpY2UiXX0.KFO-37xi0z086lbdOzRKNZBijDVSi4dlpdFVzhHvXkvbypsEGLIrurntWf5UhQaFZ9xB8JPGIgjvbybfrpZxWwTJgX04NpXSkrATBsQucI-J181lhuHeefwLDfPsAIRP4QGbzbgLZ_4RrAdi66PU2oKIYV0-REUIhtRNzJhUFCZckWpa2pLo0hwzq8gzBVFoOrsWtwTeDrGKc3F7RWCsDJeByGvyBfI33n6r3S6XOSt0aNvLBrihqBAqPgudWeCHO-4gQ5MBh7SCz9H-oO92vviNaiEVklEJP24l52R0TTFsxky4YbUsozPU6YXyoxa5o2dxJo_pWoek-GmdW7_YJw","token_type":"bearer","refresh_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ0cyIsInNjb3BlIjpbInNlcnZpY2UiXSwiYXRpIjoiY2RiYTUxMTEtOWY2YS00ZTU1LThmZGEtNTNjMDM5ZjE5ZGIyIiwiZXhwIjoxNTMzNDM5NjYzLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiLCJST0xFX0FETUlOIiwiQURNSU4iXSwianRpIjoiYzAxMGY4MmUtZDRkYS00MTNmLWEwMTctZTM0MzA2YWY2OWViIiwiY2xpZW50X2lkIjoidXNlci1zZXJ2aWNlIn0.RKe3rjgrl3Hu1jAVa68csSJ-Y2b75LWYgke5urscQGv2OH7dOuOmcyUo9K_dfvT9Jz9WNDdz-rmdCBfw7bPdoDfCh4wCi-2Xh0ufl6Q4RO6eWLGSpcA2x7-dJsh325Ylje6PC3-__ID_SS1znM4zw_xBubp1Uah0hpuEkqtKUgPWOnV4eybvGvJlSqbZLhenCQrhYCrWW781jYkCKm8E6AoQHUyVRrQ_jiyfcfYQs9wEuJNtuZXwoYIW4xM-hDr1rVkPab8thjZ3EkVnIgoTXo0t_i_SiVWCrNo2874QZq8BBj3-St7YyW_JyQM0jGT5VrgkcbCiuCZebDdyIBBAdQ","expires_in":3599,"scope":"service","jti":"cdba5111-9f6a-4e55-8fda-53c039f19db2"}%
构建 user 微服务
同样构建 maven module,名称是 cnsesan-user-service
pom 依赖和上面的 uaa 类似,多了如下2个依赖
org.springframework.cloud
spring-cloud-starter-hystrix
org.springframework.cloud
spring-cloud-starter-feign
配置文件 application.yml
server:
port: 9090
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8762/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: root
jpa:
hibernate:
ddl-auto: update
show-sql: true
feign:
hystrix:
enabled: true
同时把 public.cert拷贝一份到 resource 目录
接下来还是先编写启动类
@EnableFeignClients
@SpringBootApplication
@EnableEurekaClient
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class)
.web(true).run(args);
}
}
配置资源服务器
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter{
@Autowired 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);
}
}
配置 JWT
@Configuration
public class JwtConfig {
@Autowired JwtAccessTokenConverter jwtAccessTokenConverter;
@Bean
@Qualifier("tokenStore")
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Bean
public JwtAccessTokenConverter jwtTokenEnhancer(){
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();
}
converter.setVerifierKey(publicKey);
return converter;
}
}
配置 开启方法级别安全验证
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启方法级别安全验证
public class GlobalMethodSecurityConfig {
}
编写用户相关服务,用户注册和用户登录
@Service
public class UserServiceDetail {
@Autowired
private UserDao userRepository;
@Autowired
AuthServiceClient client;
public User insertUser(String username,String password){
User user=new User();
user.setUsername(username);
user.setPassword(BPwdEncoderUtil.BCryptPassword(password));
return userRepository.save(user);
}
public UserLoginDTO login(String username,String password){
User user=userRepository.findUserByUsername(username);
if(user==null){
throw new RuntimeException("用户不存在");
}
if(!BPwdEncoderUtil.matches(password, user.getPassword())){
throw new RuntimeException("用户密码不对");
}
//dXNlci1zZXJ2aWNlOjEyMzQ1Ng== 是 user-service:123456的 base64编码
JWT jwt=client.getToken("Basic dXNlci1zZXJ2aWNlOjEyMzQ1Ng==", "password", username, password);
if(jwt==null){
throw new RuntimeException("用户Token有问题");
}
UserLoginDTO dto=new UserLoginDTO();
dto.setUser(user);
dto.setJwt(jwt);
return dto;
}
}
上面服务有个AuthServiceClient类,他是个接口,使用Feign向 uaa 去请求,同时加以熔断机制进行处理
@FeignClient(value="uaa-service", fallback =AuthServiceHystrix.class )
public interface AuthServiceClient {
@PostMapping(value ="/oauth/token")
JWT getToken(@RequestHeader(value="Authorization")String authorization,
@RequestParam("grant_type")String type,
@RequestParam("username")String username,
@RequestParam("password")String password);
}
而AuthServiceHystrix是一个默认的处理方式
@Component
public class AuthServiceHystrix implements AuthServiceClient{
@Override
public JWT getToken(String authorization, String type, String username, String password) {
// TODO Auto-generated method stub
return null;
}
}
JWT 是一个 POJO 类
public class JWT {
private String access_token,token_type,refresh_token,scope,jti;
private int expires_in;
//set和 get
}
UserDao,User,Role和之前的 uaa 项目一样,不在赘述。
针对异常做统一处理
@ControllerAdvice
@ResponseBody
public class ExceptionHandle {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity handleException(Exception e){
return new ResponseEntity (e.getMessage () , HttpStatus.OK) ;
}
}
编写我们的控制层的类
@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);
}
@PostMapping ("/login")
public UserLoginDTO login(@RequestParam ("username")String username,@RequestParam ("password")String password){
return userServiceDetail.login(username, password);
}
}
其中涉及的一个工具类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);
}
}
到此为止用户服务编写完毕,我们开始测试,打开 postman 工具
先注册一个用户
http://localhost:9090/user/register?username=shun&password=123456
使用 Post 方式,输入http://localhost:9090/user/login?username=shun&password=123456,首先需要数据库有这样的数据
可以看到
之后的访问需要带上我们的 Token 令牌
编写个测试 Controller
@RestController
public class DemoController {
@RequestMapping("/hi")
public String hi(){
return "hi,你好";
}
@RequestMapping("/hello")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String hello(){
return "hello,你好";
}
@RequestMapping("/getPrincipal")
public OAuth2Authentication getPrinciple(OAuth2Authentication oauth2Authentication,Principal principal,Authentication authentication){
System.out.println("====================================");
System.out.println(oauth2Authentication);
System.out.println(principal);
System.out.println(authentication);
System.out.println("====================================");
return oauth2Authentication;
}
}
我们直接访问 http://localhost:9090/hi
我们需要在请求头增加 Token
这样才可以正常访问
但是如果需要 admin 权限的,即使带上也是访问不了的
我们可以测试http://localhost:9090/hello,这个接口需要 ROLE_ADMIN 权限
我们切换另外一个用户