手记

基于 Spring Security OAuth2和 JWT 构建保护微服务系统

我们希望自己的微服务能够在用户登录之后才可以访问,而单独给每个微服务单独做用户权限模块就显得很弱了,从复用角度来说是需要重构的,从功能角度来说,也是欠缺的。尤其是前后端完全分离之后,我们的用户信息不一定存在于 Session 会话中,本节内容使用OAuth2+JWT的功能恰好能够弥补这块。

应用场景

常见的应用场景如下图,用户通过浏览器进行登录,一旦确定用户名和密码正确,那么在服务器端使用秘钥创建 JWT,并且返回给浏览器;接下来我们的请求需要在头部增加 jwt 信息,服务器端进行解密获取用户信息,然后进行其他业务逻辑处理,再返回客户端


image

实战案例

我们基于 Spring Cloud 的骨架进行搭建,分为3个工程,eureka 服务器,负责微服务注册;auth 服务器,负责授权,需要提供 clientId 和密码;user 微服务,一个微服务提供,他作为资源服务器,资源是被保护起来的,需要相应的权限才能访问。User 微服务得到用户请求的 JWT 之后,使用公钥解密,得到用户信息和权限信息。


image

编写主 maven 工程

构建一个 maven 项目,打包类型是 pom,其中该 pom 文件内容如下

<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>
    <groupId>com.cnsesan</groupId>
    <artifactId>cnsean-architecture-spring-cloud</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.13.RELEASE</version>
        <relativePath/>
    </parent>


    <properties>
        <security.version>1.0.0-SNAPSHOT</security.version>
        <java.version>1.8</java.version>
    </properties>

    <!-- 替我们管理依赖的版本信息 -->
    <dependencyManagement>
        <dependencies>
            <!-- spring io -->
            <dependency>
                <groupId>io.spring.platform</groupId>
                <artifactId>platform-bom</artifactId>
                <version>Brussels-SR11</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- spring cloud -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.SR5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <compilerVersion>1.8</compilerVersion>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>


    <modules>
        <module>cnsesan-eureka-single</module>
        <module>cnsesan-uaa-service</module>
        <module>cnsesan-user-service</module>
    </modules></project>

上述的版本是经过测试可以正常使用的,如果需要更新到 SpringBoot2.0版本,需要更新其他版本进行对应。同时也看到该 pom 内部包含3个 module,接下来我们分别来构建这3个 module。

构建 EurekaServer

这里我们构建的是单个 Eureka 服务器作为测试,真实环境是需要集群的。在父项目的基础上,右键构建,如下图(IDE 为 STS)


image


image

配置 pom,加入依赖

<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>com.cnsesan</groupId>
    <artifactId>cnsean-architecture-spring-cloud</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <artifactId>cnsesan-eureka-single</artifactId>
  <dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka-server</artifactId>
    </dependency>
    </dependencies></project>

这里仅仅引入 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: 5000logging:
  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@SpringBootApplicationpublic class Application {    public static void main(String[] args) {        new SpringApplicationBuilder(Application.class)
                    .web(true).run(args);
    }
}

找到BootDashboard,运行eureka


image

构建 Uaa 授权服务

同样构建 maven 项目,导入依赖,pom 文件为

<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>com.cnsesan</groupId>
    <artifactId>cnsean-architecture-spring-cloud</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <artifactId>cnsesan-uaa-service</artifactId>
  
  <dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-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-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
  </dependencies>
  
  
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <configuration>
                    <nonFilteredFileExtensions>cert</nonFilteredFileExtensions>
                    <nonFilteredFileExtensions>jks</nonFilteredFileExtensions>
                </configuration>
            </plugin>
        </plugins>
    </build>
  </project>

其中最后一段是防止打包的时候把公钥和私钥文件搞乱,读取不了。

接下来配置 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: trueserver:
  port: 9999eureka:
  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


image


可以得到public.cert
将这两个文件拷贝到 resource 目录下


image

接下来首先编写启动类,主要是几个注解

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@EnableEurekaClientpublic 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,他的代码如下,主要是负责用户信息获取的

@Servicepublic 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, Long>{ User findByUsername(String username);
}

User 和 Role 两个实体类需要做如下的实现

@Entitypublic 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<Role> 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<Role> 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;
    }

}
@Entitypublic 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个依赖

    <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>

配置文件 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@EnableEurekaClientpublic class Application {    public static void main(String[] args) {        new SpringApplicationBuilder(Application.class)
        .web(true).run(args);
    }
}

配置资源服务器

@Configuration        @EnableResourceServerpublic 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

@Configurationpublic 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 {
}

编写用户相关服务,用户注册和用户登录

@Servicepublic 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是一个默认的处理方式

@Componentpublic 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@ResponseBodypublic class ExceptionHandle {    @ExceptionHandler(RuntimeException.class)    public ResponseEntity<String> 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

image


使用 Post 方式,输入http://localhost:9090/user/login?username=shun&password=123456,首先需要数据库有这样的数据
可以看到

image


之后的访问需要带上我们的 Token 令牌


编写个测试 Controller

@RestControllerpublic 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

image


我们需要在请求头增加 Token


image


这样才可以正常访问

但是如果需要 admin 权限的,即使带上也是访问不了的
我们可以测试http://localhost:9090/hello,这个接口需要 ROLE_ADMIN 权限

image


我们切换另外一个用户


image


image



作者:breezedancer
链接:https://www.jianshu.com/p/24764aba1012

3人推荐
随时随地看视频
慕课网APP

热门评论

想问一下,我这边获取到了access_token了,携带token过去访问资源服务的时候返回

{

    "error": "invalid_token",

    "error_description": "Cannot convert access token to JSON"

}

这个可能是什么原因呢

想问一下,我这边获取到了access_token了,携带token过去访问资源服务的时候返回

{

    "error": "invalid_token",

    "error_description": "Cannot convert access token to JSON"

}

这个可能是什么原因呢

源码能共享么?

查看全部评论