spring security + oauth2 + jwt + gateway 實現(xiàn)統(tǒng)一身份認證
├──security-user --認證服務(wù)
├──gateway-forward --統(tǒng)一網(wǎng)關(guān)
├──order-server --資源服務(wù)
├──resources --數(shù)據(jù)庫等靜態(tài)資源
基礎(chǔ)講解
四種授權(quán)模式
"authorization_code", "password", "client_credentials", "implicit", "refresh_token"
- 密碼模式(resource owner password credentials)(為遺留系統(tǒng)設(shè)計)(支持refresh token)
- 授權(quán)碼模式(authorization code)(正宗方式)(支持refresh token)
- 簡化模式(implicit)(為web瀏覽器應(yīng)用設(shè)計)(不支持refresh token)
- 客戶端模式(client credentials)(為后臺api服務(wù)消費者設(shè)計)(不支持refresh token)
- 授權(quán)碼模式 authorization_code
- 這種模式算是正宗的oauth2的授權(quán)模式, 最安全
- 設(shè)計了auth code,通過這個code再獲取token
- 支持refresh token
授權(quán)碼的請求路徑
/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_url=http://www.baidu.com
獲取token
/oauth/token?client_id=c1&response_type=code&scope=all&redirect_url=http://www.baidu.com
- 簡化模式 implicit
- 這種模式比授權(quán)碼模式少了code環(huán)節(jié)壶运,回調(diào)url直接攜帶token
- 這種模式的使用場景是基于瀏覽器的應(yīng)用
- 這種模式基于安全性考慮嗦哆,建議把token時效設(shè)置短一些
- 不支持refresh token
經(jīng)過授權(quán)后直接返回access_token,
/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_url=http://www.baidu.com
返回值
https://www.baidu.com/#access_token=20ea0a59-a043-492f-86cf-836f948f58cc&token_type=bearer&expires_in=6637
https://www.baidu.com/#access_token=access_toke&token_type=bearer&expires_in=7199&jti=9c7765db-f140-4545-a4ae-3f8cc7b6ba6e
- 密碼模式 password
- 這種模式是最不推薦的硬萍,會將密碼泄露給client
- 這種模式主要用來做遺留項目升級為oauth2的適配方案
- 當然如果client是自家的應(yīng)用,也是可以
- 支持refresh token
post 請求
/oauth/token
參數(shù)
client_id=c1&client_secret=secret&grant_type=password&username=admin&password=1111
- 客戶端模式 client_credentials
- 這種模式直接根據(jù)client的id和密鑰即可獲取token疗锐,無需用戶參與
- 這種模式比較合適消費api的后端服務(wù),比如拉取一組用戶信息等
- 不支持refresh token,主要是沒有必要
refresh token的初衷主要是為了用戶體驗不想用戶重復(fù)輸入賬號密碼來換取新token畴椰,因而設(shè)計了refresh token用于換取新token
這種模式由于沒有用戶參與,而且也不需要用戶賬號密碼鸽粉,僅僅根據(jù)自己的id和密鑰就可以換取新token斜脂,因而沒必要refresh token
post 請求
/oauth/token
參數(shù)
client_id=c1&client_secret=secret&grant_type=client_credentials
資源服務(wù)
- 校驗token
/oauth/check_token?token=289d1f2d-26f5-4f2d-9542-6c4d3f0a936c
訪問資源
http://localhost:8205/order/r/r1?abc=123
網(wǎng)關(guān)整合auth2
網(wǎng)關(guān)整合auth2有兩種方式
- 認證服務(wù)器生成jwt令牌,所有請求統(tǒng)一在網(wǎng)關(guān)層驗證触机,判斷權(quán)限
- 由資源服務(wù)處理帚戳,網(wǎng)關(guān)只做請求轉(zhuǎn)發(fā)
使用spring gateway作為網(wǎng)關(guān)
- 建立認證服務(wù)器
1.1. 父工程依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<finalName>${project.name}</finalName>
<resources>
<resource>
<directory>src/main/resource</directory>
<filtering>true</filtering>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
</configuration>
</plugin>
</plugins>
</build>
1.2. 認證服務(wù)器依賴
<dependencies>
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<!--<exclusions>-->
<!--<exclusion>-->
<!--<groupId>org.springframework.security.oauth.boot</groupId>-->
<!--<artifactId>spring-security-oauth2-autoconfigure</artifactId>-->
<!--</exclusion>-->
<!--</exclusions>-->
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
1.3. 配置文件 application.yml
server:
port: 8202
spring:
application:
name: user-server
datasource:
url: jdbc:mysql://localhost:3306/user-center?serverTimezone=PRC&useUnicode=true&characterEncoding=utf8&useSSL=true
username: root
password:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
main:
allow-bean-definition-overriding: true
1.4. 認證服務(wù)器配置(核心類AuthorizationServerConfigurerAdapter)
認證服務(wù)器配置類必須繼承AuthorizationServerConfigurerAdapter類
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import javax.sql.DataSource;
import java.util.Arrays;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
//
// @Bean
// public AuthorizationCodeServices authorizationCodeServices()
// {
// return new InMemoryAuthorizationCodeServices();
// }
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource)
{
return new JdbcAuthorizationCodeServices(dataSource);
}
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource)
{
JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
clientDetailsService.setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
// 配置客戶端詳細信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// clients.inMemory() // 使用內(nèi)存存儲
//
// .withClient("c1") // client_id
// // 客戶端密鑰
// .secret(new BCryptPasswordEncoder().encode("secret"))
// // 資源列表
// .resourceIds("res1")
// // 授權(quán)方式
// .authorizedGrantTypes("authorization_code","password","client_credentials","implicit","refresh_token")
// // 允許授權(quán)的范圍
// .scopes("all")
// //
// .autoApprove(false) // false 跳轉(zhuǎn)到授權(quán)頁面
// // 加上驗證回調(diào)地址
// .redirectUris("http://www.baidu.com");
clients.withClientDetails(clientDetailsService);
}
// 令牌管理服務(wù)
public AuthorizationServerTokenServices tokenServices()
{
DefaultTokenServices service = new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService); // 客戶端信息服務(wù)
service.setSupportRefreshToken(true); // 是否產(chǎn)生刷新令牌
service.setTokenStore(tokenStore); // 設(shè)置令牌存儲策略
// 令牌增強
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200);// 令牌默認有效期 2 小時
service.setRefreshTokenValiditySeconds(259200);
return service;
}
/**
* /oauth/authorize 授權(quán)端點
* /oauth/token 令牌斷點
* /oauth/confirm-access用戶確認授權(quán)提交端點
* /auth/error 授權(quán)服務(wù)錯誤信息斷電
* /auth/check_token 用戶資源服務(wù)訪問的令牌解析斷電
* /oauth/token_key 提供公有密鑰的端點,如果你使用jwt令牌的話
*/
/**
* 令怕i訪問端點
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// 密碼管理模式
.authenticationManager(authenticationManager)
// 授權(quán)碼模式
.authorizationCodeServices(authorizationCodeServices)
.tokenServices(tokenServices()) // 令牌管理服務(wù)
.allowedTokenEndpointRequestMethods(HttpMethod.POST); // 允許post提交
}
/**
* 令牌訪問端點的安全策略
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()") // /oauth/token_key 公開
.checkTokenAccess("permitAll()") // /auth/check_token 檢測令牌
.allowFormAuthenticationForClients(); // 允許通過表單認證儡首,申請令牌
}
}
1.4.1. 內(nèi)存存儲token方案
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
public class TokenConfig {
//內(nèi)存存儲token方案
// @Bean
// public TokenStore tokenStore() {
// return new InMemoryTokenStore();
// }
//jwt令牌存儲方案
private static final String SIGNING_KEY = "uaa123";
// 令牌存儲策略
@Bean
public TokenStore tokenStore()
{
// Jwt令牌存儲方案
return new JwtTokenStore(assessTokenConverter());
}
@Bean
public JwtAccessTokenConverter assessTokenConverter()
{
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);//
return converter;
}
}
1.4.2. 配置Spring Security的安全認證
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//認證管理器
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 安全攔截機制
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
// .antMatchers("/r/r1").hasAnyAuthority("p1")
.antMatchers("/login*").permitAll()
.anyRequest().authenticated()
.and().formLogin()
;
}
}
1.4.3. 自定義ClientDetailsService實現(xiàn)類
import com.exercise.dao.UserDao;
import com.exercise.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
UserDto bean = userDao.getUserByUsername(s);
if (bean == null)
return null;
List<String> permissions = userDao.findPermissionByUserId(bean.getId());
String[] permissionArray = new String[permissions.size()];
permissions.toArray(permissionArray);
UserDetails userDetails = User.withUsername(bean.getUsername()).password(bean.getPassword()).authorities(permissionArray).build();
return userDetails;
}
}
1.4.4. 相關(guān)實體類
UserDao:
import com.exercise.model.PermissionDto;
import com.exercise.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
@Repository
public class UserDao {
@Autowired
JdbcTemplate jdbcTemplate;
public UserDto getUserByUsername(String usernmae)
{
String sql = "select id,username,password,fullname from sys_user where username = ?";
List<UserDto> result = jdbcTemplate.query(sql, new Object[]{usernmae}, new BeanPropertyRowMapper<>(UserDto.class));
if (result== null && result.size() <= 0)return null;
return result.get(0);
}
// 根據(jù)用戶id 查詢用戶權(quán)限
public List<String> findPermissionByUserId(String userId)
{
String sql = "select id , permission as code, permission as url, name as description from sys_permission where id in (" +
"select permission_id from sys_role_permission where role_id in (" +
"select role_id from sys_role_user where user_id = ?" +
")" +
")";
List<PermissionDto> result = jdbcTemplate.query(sql, new Object[]{userId}, new BeanPropertyRowMapper<>(PermissionDto.class));
List<String> permissions = new ArrayList<String>();
result.forEach( p->permissions.add(p.getCode()) );
return permissions;
}
}
PermissionDto:
import lombok.Data;
@Data
public class PermissionDto {
private String id;
private String code;
private String description;
private String url;
}
UserDto:
import lombok.Data;
@Data
public class UserDto {
private String id;
private String username;
private String password;
private String fullname;
private String phone;
private String nickName;
}
1.5. gateway 網(wǎng)關(guān)
1.5.1 pom依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- security-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
1.5.2. application.yml配置文件
#端口
server:
port: 8205
spring:
application:
name: gateway-forward
cloud:
gateway:
routes:
# 路由標識(id:標識片任,具有唯一性) 簡單嘗試
- id: auth-server
uri: http://localhost:8202
order: 8202
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
- id: order-server
uri: http://localhost:8201
order: 8201
predicates:
- Path=/order/**
filters:
- StripPrefix=1
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8200/eureka/
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true
logging:
level:
# log 級別
com.xxx: debug
1.5.3. security安全配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@EnableWebFluxSecurity
@Configuration
public class WebSecurityConfig {
@Bean
public SecurityWebFilterChain webFluxFilterChain(ServerHttpSecurity http) {
http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/**").permitAll()
// //option 請求默認放行
// .pathMatchers(HttpMethod.OPTIONS).permitAll()
.and()
.formLogin()
;
return http.build();
}
}
1.5.4. gateway全局過濾器
@Component
@Slf4j
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (!StringUtils.isEmpty(token)) {
//從token中解析用戶信息并設(shè)置到Header中去
String realToken = token.replace("Bearer ", "");
org.springframework.security.jwt.Jwt jwt = JwtHelper.decode(realToken);
String claims = jwt.getClaims();
Map<String, Object> map = (Map<String, Object>) JSON.parse(claims);
//取出用戶身份信息
String userStr = (String) map.get("user_name");
JSONArray authoritiesArray = (JSONArray) map.get("authorities");
log.info("AuthFilter.filter() user:{}",userStr);
String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);
Map<String,Object> jsonToken = new HashMap<>();
jsonToken.put("principal",userStr);
jsonToken.put("authorities",authorities);
//把身份信息和權(quán)限信息放在json中,加入http的header中,轉(zhuǎn)發(fā)給微服務(wù)
ServerHttpRequest host = exchange.getRequest().mutate().header("json-token", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken))).build();
ServerWebExchange build = exchange.mutate().request(host).build();
return chain.filter(build);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -500;
}
protected class JwtUtil implements JwtDecoder {
@Override
public Jwt decode(String s) throws JwtException {
return null;
}
}
}
1.5.5. 其他類
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Base64;
public class EncryptUtil {
private static final Logger logger = LoggerFactory.getLogger(EncryptUtil.class);
public static String encodeBase64(byte[] bytes){
String encoded = Base64.getEncoder().encodeToString(bytes);
return encoded;
}
public static byte[] decodeBase64(String str){
byte[] bytes = null;
bytes = Base64.getDecoder().decode(str);
return bytes;
}
public static String encodeUTF8StringBase64(String str){
String encoded = null;
try {
encoded = Base64.getEncoder().encodeToString(str.getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
logger.warn("不支持的編碼格式",e);
}
return encoded;
}
public static String decodeUTF8StringBase64(String str){
String decoded = null;
byte[] bytes = Base64.getDecoder().decode(str);
try {
decoded = new String(bytes,"utf-8");
}catch(UnsupportedEncodingException e){
logger.warn("不支持的編碼格式",e);
}
return decoded;
}
public static String encodeURL(String url) {
String encoded = null;
try {
encoded = URLEncoder.encode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.warn("URLEncode失敗", e);
}
return encoded;
}
public static String decodeURL(String url) {
String decoded = null;
try {
decoded = URLDecoder.decode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.warn("URLDecode失敗", e);
}
return decoded;
}
public static void main(String [] args){
String str = "abcd{'a':'b'}";
String encoded = EncryptUtil.encodeUTF8StringBase64(str);
String decoded = EncryptUtil.decodeUTF8StringBase64(encoded);
System.out.println(str);
System.out.println(encoded);
System.out.println(decoded);
String url = "== wo";
String urlEncoded = EncryptUtil.encodeURL(url);
String urlDecoded = EncryptUtil.decodeURL(urlEncoded);
System.out.println(url);
System.out.println(urlEncoded);
System.out.println(urlDecoded);
}
}
1.6. 資源服務(wù)器
1.6.1. 依賴配置
pom.xml
<dependencies>
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</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-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
application.yml
server:
port: 8201
spring:
application:
name: order-server
1.6.2. 資源服務(wù)器配置
繼承ResourceServerConfigurerAdapter類
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "res1";
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").access("#oauth2.hasScope('ROLE_API')")
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
super.configure(http);
}
// 資源服務(wù)令牌解析服務(wù)
/**
* 使用遠程服務(wù)請求授權(quán)服務(wù)器校驗的token蔬胯,必須制定校驗token 的url对供, client_id,client_secret
* @return
*/
// @Bean
// public ResourceServerTokenServices tokenServices()
// {
// RemoteTokenServices service = new RemoteTokenServices();
// service.setCheckTokenEndpointUrl("http://localhost:8202/oauth/check_token");
// service.setClientId("c1");
// service.setClientSecret("secret");
// return service;
// }
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID) // 資源id
// .tokenServices(tokenServices()) // 驗證令牌的服務(wù)
.tokenStore(tokenStore) // 驗證令牌的服務(wù)
.stateless(true);
}
}
安全配置繼承WebSecurityConfigurerAdapter
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/r/**").authenticated()
.anyRequest().permitAll();
}
}
1.6.3 token過濾器
獲取從網(wǎng)關(guān)處轉(zhuǎn)發(fā)的token,填充到認證的安全上下文中笔宿,實現(xiàn)身份權(quán)限識別
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.exercise.model.UserDTO;
import com.exercise.util.EncryptUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class TokenAutnenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//解析出頭中的token
String token = httpServletRequest.getHeader("json-token");
if(token!=null){
String json = EncryptUtil.decodeUTF8StringBase64(token);
//將token轉(zhuǎn)成json對象
JSONObject jsonObject = JSON.parseObject(json);
//用戶身份信息
UserDTO userDTO = new UserDTO();
String principal = jsonObject.getString("principal");
userDTO.setUsername(principal);
// UserDTO userDTO = JSON.parseObject(jsonObject.getString("principal"), UserDTO.class);
//用戶權(quán)限
JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);
//將用戶信息和權(quán)限填充 到用戶身份token對象中
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
//將authenticationToken填充到安全上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
使用zuul作為網(wǎng)關(guān)
- 設(shè)置依賴和配置
pom.xml
<dependencies>
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</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-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
application.yml
#端口
server:
port: 8204
spring:
application:
name: zuul-server
zuul:
routes:
order-server:
path: /order/**
url: http://localhost:8201/
user-server:
path: /auth/**
url: http://localhost:8202/
retryable: true
add-host-header: true
#不過濾HTTP請求頭信息
sensitive-headers:
- 配置資源服務(wù)
2.1. 資源服務(wù)配置
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
@Configuration
public class ResourceServerConfig {
public static final String RESOURCE_ID = "res1";
//user資源服務(wù)配置
@Configuration
@EnableResourceServer
public class UAAServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/auth/**").permitAll();
}
}
//order資源
//uaa資源服務(wù)配置
@Configuration
@EnableResourceServer
public class OrderServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/order/**").access("#oauth2.hasScope('ROLE_API')");
}
}
//配置其它的資源服務(wù)..
}
2.2. 安全配置
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.and().csrf().disable();
}
}
2.3. 其他配置
@Configuration
public class ZuulConfig {
@Bean
public AuthFilter preFileter() {
return new AuthFilter();
}
@Bean
public FilterRegistrationBean corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setMaxAge(18000L);
source.registerCorsConfiguration("/**", config);
CorsFilter corsFilter = new CorsFilter(source);
FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
2.4. 身份信息過濾
import com.alibaba.fastjson.JSON;
import com.exercise.util.EncryptUtil;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class AuthFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
return true;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
//從安全上下文中拿 到用戶身份對象
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(!(authentication instanceof OAuth2Authentication)){
return null;
}
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
//取出用戶身份信息
String principal = userAuthentication.getName();
//取出用戶權(quán)限
List<String> authorities = new ArrayList<>();
//從userAuthentication取出權(quán)限辐脖,放在authorities
userAuthentication.getAuthorities().stream().forEach(c->authorities.add(((GrantedAuthority) c).getAuthority()));
OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
Map<String,Object> jsonToken = new HashMap<>(requestParameters);
if(userAuthentication!=null){
jsonToken.put("principal",principal);
jsonToken.put("authorities",authorities);
}
//把身份信息和權(quán)限信息放在json中,加入http的header中,轉(zhuǎn)發(fā)給微服務(wù)
ctx.addZuulRequestHeader("json-token", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));
return null;
}
}