採用Spring Security AOuth2 和 JWT 的方式,避免每次請求都須要遠程調度 Uaa 服務。採用Spring Security OAuth2 和 JWT 的方式,Uaa 服務只驗證一次,返回JWT。返回的 JWT 包含了用戶的全部信息,包括權限信息。html
JSON Web Token(JWT)是一種開放的標準(RFC 7519),JWT定義了一種緊湊且自包含的標準,該標準旨在將各個主體的信息包裝爲 JSON 對象。主體信息是經過數字簽名進行加密和驗證的。常使用 HMAC 算法或 RSA(公鑰/私鑰的非對稱性加密)算法對JWT進行簽名,安全性很高。java
JWT 特色:mysql
JWT結構:web
所以,JWT的一般格式是:xxxxx.yyyyy.zzzzz算法
(1)Headerspring
Header 一般是由兩部分組成:令牌的類型(即JWT)和使用的算法類型,如 HMAC、SHA256和RSA。例如:sql
{ "typ": "JWT", "alg": "HS256" }
將 Header 用 Base64 編碼做爲 JWT 的第一部分。數據庫
(2)Payloadapache
這是 JWT 的第二部分,包含了用戶的一些信息和Claim(聲明、權利)。有3類型的 Claim:保留、公開和私人。json
{ "sub": "123456789", "name": "John Doe", "admin": true }
將 Payload 用 Base64 編碼做爲 JWT 的第一部分。
(3)Signature
要建立簽名部分,須要將 Base64 編碼後的 Header、Payload 和祕鑰進行簽名,一個典型的格式以下:
HMACSHA256( base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret )
認證流程圖以下,客戶端獲取JWT後,之後每次請求都不須要再經過Uaa服務來判斷該請求的用戶以及該用戶的權限。在微服務中,能夠利用JWT實現單點登陸。
三個工程:
工程架構圖:
1.新建Spring Boot工程,取名爲 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>com.example</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>
2.配置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/
3.配置Spring Security
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override @Bean public 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(); } @Autowired UserServiceDetail userServiceDetail; @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); } }
UserDao.java
@Repository public interface UserDao extends JpaRepository<User, Long> { User findByUsername(String username); }
User對象和上一篇文章的內容同樣,須要實現UserDetails接口,Role對象須要實現GrantedAuthority接口.
@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 User() { } 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; } }
@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; } }
4.配置 Authorization Server
在 OAuth2Config 這個類中配置 AuthorizationServer,其代碼以下:
@Configuration @EnableAuthorizationServer public class OAuth2Config extends AuthorizationServerConfigurerAdapter { @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() //將客戶端的信息存儲在內存中 .withClient("user-service") //建立了一個Client爲"user-service"的客戶端 .secret("123456") .scopes("service") //客戶端的域 .authorizedGrantTypes("refresh_token", "password") //配置類驗證類型爲 refresh_token和password .accessTokenValiditySeconds(12*300); //5min過時 } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtTokenEnhancer()).authenticationManager(authenticationManager); } @Autowired @Qualifier("authenticationManagerBean") private 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; } }
5.生成 jks 文件
配置 JwtTokenStore 時須要使用 jks 文件做爲 Token 加密的祕鑰。
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 選項爲別名,-keypass 和 -storepass 爲密碼選項,-validity 爲配置jks文件過時時間(單位:天)。
獲取的 jks 文件做爲私鑰,只容許 Uaa 服務持有,並用做加密 JWT。也就是把生成的 jks 文件放到 auth-service 工程的resource目錄下。那麼 user-service 這樣的資源服務,是如何解密 JWT 的呢?這時就須要使用 jks 文件的公鑰。獲取 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 等資源服務的resources目錄下。到目前爲止,Uaa 服務已經搭建完畢。
須要注意的是,Maven 在項目編譯時,可能會將 jks 文件編譯,致使 jks 文件亂碼,最後不可用。須要在工程的 pom 文件中添加如下內容:
<!--防止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>
最後,別忘了在啓動類註解@EnableEurekaClient開啓服務註冊.
@SpringBootApplication @EnableEurekaClient public class AuthServiceApplication { public static void main(String[] args) { SpringApplication.run(AuthServiceApplication.class, args); } }
1.新建Spring Boot工程,取名爲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>com.example</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>
2.配置文件application.yml
在工程的配置文件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
3.配置Resource Server
在配置Resource Server以前,須要注入 JwtTokenStore 類型的 Bean。
@Configuration public class JwtConfig { @Autowired JwtAccessTokenConverter jwtAccessTokenConverter; @Bean @Qualifier("tokenStore") public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter); } @Bean protected 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; } }
而後配置 Resource Server
@Configuration @EnableResourceServer //開啓Resource Server功能 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); } }
4.新建一個配置類 GlobalMethodSecurityConfig,在此類中經過 @EnableGlobalMethodSecurity(prePostEnabled = true)註解開啓方法級別的安全驗證。
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class GlobalMethodSecurityConfig { }
5.編寫用戶註冊接口
拷貝auth-service工程的User.java、Role.java 和 UserDao.java 到本工程。
在 Service 層的 UserService 寫一個插入用戶的方法,代碼以下
@Service public class UserServiceDetail { @Autowired private UserDao 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); } }
在 Web 層,在 Controller 中寫一個註冊的 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); } }
6.編寫用戶登陸接口
在Service層,在 UserServiceDetail 中添加一個 login(登陸)方法,代碼以下:
@Service public class UserServiceDetail { @Autowired private AuthServiceClient client; public UserLoginDTO login(String username, String password){ User user=userRepository.findByUsername(username); if (null == user) { throw new UserLoginException("error username"); } if(!BPwdEncoderUtil.matches(password,user.getPassword())){ throw new UserLoginException("error password"); } // 獲取token 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 經過向 auth-service 服務遠程調用「/oauth/token」 API接口,獲取 JWT。在 "/oauth/token" API 接口,獲取JWT。在「/oauth/token」API接口中須要在請求頭傳入 Authorization 信息,並須要傳請求參數認證類型 grant_type、用戶名 username 和密碼 password,代碼以下:
@FeignClient(value = "auth-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 爲AuthServiceClient 的熔斷器,代碼以下:
@Component public class AuthServiceHystrix implements AuthServiceClient { @Override public JWT getToken(String authorization, String type, String username, String password) { return null; } }
JWT 爲一個 JavaBean,它包含了 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; //getter setter
UserLoginDTO 包含了一個 User 和一個 JWT 對象,用於返回數據的實體:
public class UserLoginDTO { private JWT jwt; private User user; //setter getter }
登陸異常類 UserLoginException
public class UserLoginException extends RuntimeException{ public UserLoginException(String message) { super(message); } }
統一異常處理
@ControllerAdvice @ResponseBody public class ExceptionHandle { @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(); }
最後,在啓動類註解開啓Feign:
@SpringBootApplication @EnableFeignClients @EnableEurekaClient public class UserServiceApplication { public static void main(String[] args) { SpringApplication.run(UserServiceApplication.class, args); } }
通過千辛萬苦,終於搭建了一個Demo工程,如今開始依次啓動 eureka-server、auth-service 和 user-service工程。這裏咱們使用PostMan測試編寫的接口。
1.註冊用戶
2.登陸獲取Token
3.訪問/user/foo
複製 access_token到 Header頭部,發起GET請求。
"Authorization":"Bearer {access_token}"
由於沒有權限,訪問被拒絕,咱們手動在數據庫添加"ROLE_ADMIN"權限,並與該用戶關聯。從新登陸並獲取Token,從新請求「/user/foo」接口
在本案例中,用戶經過登陸接口來獲取受權服務的Token 。用戶獲取Token 成功後,在之後每次訪問資源服務的請求中都須要攜帶該Token 。資源服務經過公鑰解密Token ,解密成功後能夠獲取用戶信息和權限信息,從而判斷該Token 所對應的用戶是誰, 具備什麼權限。
這個架構的優勢在於,一次獲取Token , 屢次使用,再也不每次詢問Uaa 服務該Token 所對應的用戶信息和用戶的權限信息。這個架構也有缺點,例如一旦用戶的權限發生了改變, 該Token 中存儲的權限信息並無改變, 須要從新登陸獲取新的Token 。就算從新獲取了Token,若是原來的Token 沒有過時,仍然是可使用的,因此須要根據具體的業務場景來設置Token的過時時間。一種改進方式是將登陸成功後獲取的Token 緩存在網關上,若是用戶的權限更改,將網關上緩存的Token 刪除。當請求通過網關,判斷請求的Token 在緩存中是否存在,若是緩存中不存在該Token ,則提示用戶從新登陸。
參考:《深刻理解Spring Cloud與微服務構建》方誌朋