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

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

编写主 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)


配置 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: 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 文件为
<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: 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, Long>{
 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<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;
    }
}
@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个依赖
    <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
@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<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

使用 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 权限

我们切换另外一个用户












