文章目录
鉴权服务基础配置鉴权配置
网关服务基础配置网关过滤器配置权限校验过滤器配置
接口调试前端适配
鉴权服务
OAuth 2.0 的四种方式OAuth2实现分析
基础配置
新建house-oauth模块,依赖oauth2。
<dependencies>
<dependency>
<groupId>com.babyjuan
</groupId>
<artifactId>house-common
</artifactId>
<version>${house.version}
</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud
</groupId>
<artifactId>spring-cloud-starter-oauth2
</artifactId>
</dependency>
</dependencies>
bootstrap.properties初始化依赖的nacos配置。
spring.application.name=house-oauth
spring.profiles.active=oauth-application
spring.cloud.nacos.config.namespace=${HOUSE_ENV}
spring.cloud.nacos.config.group=house
spring.cloud.nacos.config.server-addr=${NACOS_ADDR}
spring.cloud.nacos.config.prefix=house
spring.cloud.nacos.config.file-extension=yaml
nacos的house-oauth-application.yaml中配置端口为6001。
server:
port: 6001
spring:
application:
name: oauth2
-server
鉴权配置
初始化基础配置:认证管理器、加密方式。
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager
authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder
passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http
) throws Exception
{
http
.authorizeRequests().antMatchers("/**").permitAll();
}
}
继承spring-security的UserDetailsService,管理用户密码、角色信息。
@Service
public class UserServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder
;
@Override
public UserDetails
loadUserByUsername(String username
) throws UsernameNotFoundException
{
if ("admin".equals(username
)) {
String role
= "ROLE_ADMIN";
List
<SimpleGrantedAuthority> authorities
= new ArrayList<>();
authorities
.add(new SimpleGrantedAuthority(role
));
String password
= passwordEncoder
.encode("123456");
return new User(username
, password
, authorities
);
}
throw new UsernameNotFoundException("no user");
}
}
配置默认资源(即接口)管理服务。如果不使用默认实现,可参考。
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
}
配置授权服务。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
public PasswordEncoder passwordEncoder
;
@Autowired
public UserDetailsService userDetailsService
;
@Autowired
private AuthenticationManager authenticationManager
;
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints
) throws Exception
{
endpoints
.authenticationManager(authenticationManager
).userDetailsService(userDetailsService
);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients
) throws Exception
{
clients
.inMemory()
.withClient("gateway-client").secret(passwordEncoder
.encode("123456"))
.authorizedGrantTypes("refresh_token", "authorization_code", "password")
.accessTokenValiditySeconds(24 * 3600)
.scopes("all");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security
) throws Exception
{
security
.allowFormAuthenticationForClients()
.checkTokenAccess("isAuthenticated()")
.tokenKeyAccess("isAuthenticated()");
}
}
网关服务
全局过滤器拦截并处理请求。流行网关对比。网关的理解:
系统网关+业务网关。
基础配置
新建house-gateway服务,引入spring cloud gateway 依赖。
spring-boot-starter-web和gateway冲突,需要去除。
<dependencies>
<dependency>
<groupId>org.springframework.cloud
</groupId>
<artifactId>spring-cloud-starter-gateway
</artifactId>
</dependency>
<dependency>
<groupId>com.babyjuan
</groupId>
<artifactId>house-common
</artifactId>
<version>${house.version}
</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot
</groupId>
<artifactId>spring-boot-starter-web
</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
bootstrap.properties初始化依赖的nacos配置。
spring.application.name=house-gateway
spring.profiles.active=gateway-application
spring.cloud.nacos.config.namespace=${HOUSE_ENV}
spring.cloud.nacos.config.group=house
spring.cloud.nacos.config.server-addr=${NACOS_ADDR}
spring.cloud.nacos.config.prefix=house
spring.cloud.nacos.config.file-extension=yaml
nacos的house-gateway-application.yaml中
配置端口为9080。获取token的请求(/oauth/token开头)转发到鉴权服务器。业务请求转发到业务网关。
server:
port: 9080
service:
url:
house: http
://localhost
:8080
oauth: http
://localhost
:6001
spring:
cloud:
gateway:
routes:
- id: oauth
uri: $
{service.url.oauth
}
predicates:
- Path=/oauth/token
- id: house
uri: $
{service.url.house
}
predicates:
- Path=/house/**
网关过滤器配置
请求头中赋予网关服务合法客户端身份。对应鉴权服务AuthorizationServerConfig中gateway-client的用户名密码。
@Configuration
public class GatewayClientFilter implements GlobalFilter, Ordered
{
private static final String GATEWAY_CLIENT_AUTHORIZATION
= "Basic " +
Base64
.getEncoder().encodeToString("gateway-client:123456".getBytes());
@Override
public Mono
<Void> filter(ServerWebExchange exchange
, GatewayFilterChain chain
) {
ServerHttpRequest
.Builder builder
= exchange
.getRequest().mutate();
builder
.header("Authorization", GATEWAY_CLIENT_AUTHORIZATION
);
return chain
.filter(exchange
.mutate().request(builder
.build()).build());
}
@Override
public int getOrder() {
return 1;
}
}
权限校验过滤器配置
对需要鉴权的url,调用鉴权服务校验身份,通过则网关转发,否则直接返回。
@Configuration
public class AccessGatewayFilter implements GlobalFilter, Ordered
{
@Autowired
private AuthClient authClient
;
@Override
public Mono
<Void> filter(ServerWebExchange exchange
, GatewayFilterChain chain
) {
ServerHttpRequest request
= exchange
.getRequest();
String url
= request
.getPath().value();
if (authClient
.hasPermissionControl(url
)) {
if (authClient
.accessable(request
)) {
return chain
.filter(exchange
);
}
return unauthorized(exchange
);
}
return chain
.filter(exchange
);
}
private Mono
<Void> unauthorized(ServerWebExchange serverWebExchange
) {
serverWebExchange
.getResponse().setStatusCode(HttpStatus
.UNAUTHORIZED
);
DataBuffer buffer
= serverWebExchange
.getResponse()
.bufferFactory().wrap(HttpStatus
.UNAUTHORIZED
.getReasonPhrase().getBytes());
return serverWebExchange
.getResponse().writeWith(Flux
.just(buffer
));
}
@Override
public int getOrder() {
return 2;
}
}
token从url的请求参数获取。校验token的鉴权服务url后缀为/oauth/check_token。
@Component
public class AuthClient {
private static final Logger LOGGER
= LoggerFactory
.getLogger(AuthClient
.class);
private RestTemplate restTemplate
= new RestTemplate();
@Value("#{'${service.url.oauth}'+'/oauth/check_token'}")
private String checkTokenUrl
;
public boolean hasPermissionControl(String url
) {
return url
.startsWith("/house");
}
public boolean accessable(ServerHttpRequest request
) {
String token
= request
.getQueryParams().getFirst("token");
UriComponentsBuilder builder
= UriComponentsBuilder
.fromHttpUrl(checkTokenUrl
).queryParam("token", token
);
URI url
= builder
.build().encode().toUri();
HttpEntity
<?> entity
= new HttpEntity<>(request
.getHeaders());
try {
ResponseEntity
<TokenInfo> response
= restTemplate
.exchange(url
, HttpMethod
.GET
, entity
, TokenInfo
.class);
LOGGER
.info("oauth request: {}, response body: {}, reponse status: {}",
entity
, response
.getBody(), response
.getStatusCode());
return response
.getBody() != null
&& response
.getBody().isActive();
} catch (RestClientException e
) {
LOGGER
.error("oauth failed.", e
);
}
return false;
}
}
接口调试
从网关获取token。带token从网关访问业务通过鉴权。
前端适配
登录时从后端获取token,暂存在store和cookies中。
const actions
= {
login({ commit
}, userInfo
) {
const { username
, password
} = userInfo
return new Promise((resolve
, reject
) => {
login({ username
: username
.trim(), password
: password
, grant_type
: "password" }).then(response
=> {
const data
= response
commit('SET_TOKEN', data
.access_token
)
commit('SET_NAME', username
.trim())
setToken(data
.access_token
)
resolve()
}).catch(error
=> {
reject(error
)
})
})
},
}
const mutations
= {
SET_TOKEN: (state
, token
) => {
state
.token
= token
},
SET_NAME: (state
, name
) => {
state
.name
= name
}
}
export function setToken(token
) {
return Cookies
.set(TokenKey
, token
)
}
axios拦截器获取store中的token,拼接在查询参数里。
service
.interceptors
.request
.use(
config
=> {
if (store
.getters
.token
) {
let params
= config
.params
;
if (params
== null) {
params
= {};
}
params
.token
= getToken();
config
.params
= params
;
}
return config
},
error
=> {
console
.log(error
)
return Promise
.reject(error
)
}
)
export function getToken() {
return Cookies
.get(TokenKey
)
}