用户通过客户端访问项目时,前端项目会部署在nginx上,加载静态文件时直接从nginx上返回即可。
当用户在客户端操作时,需要调用后端的一些服务接口。这些接口会通过Gateway网关,网关进行一定的处理(jwt合法性校验,黑名单、白名单,过滤一部分请求)之后再转发给具体的微服务。
具体的资源服务会对请求进行解析,判断当前登录用户是否有权限调用该资源的接口。
下面两个为关键依赖,还可以自行补充nacos等依赖
<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>
AuthorizationServer :
/**
* @description 授权服务器配置 OAUTH2.0
* @author Mr.M
* @date 2022/9/26 22:25
* @version 1.0
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Resource(name="authorizationServerTokenServicesCustom")
private AuthorizationServerTokenServices authorizationServerTokenServices;
@Autowired
private AuthenticationManager authenticationManager;
//客户端详情服务
/**
* 用来配置客户端详情服务(ClientDetailsService),
* 随便一个客户端都可以随便接入到它的认证服务吗?
* 答案是否定的,服务提供商会给批准接入的客户端一个身份,用于接入时的凭据,
* 有客户端标识和客户端秘钥,在这里配置批准接入的客户端的详细信息。
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.inMemory()// 使用in-memory存储
.withClient("XcWebApp")// client_id
// .secret("XcWebApp")//客户端密钥
.secret(new BCryptPasswordEncoder().encode("XcWebApp"))//客户端密钥
.resourceIds("xuecheng-plus")//资源列表
.authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
.scopes("all")// 允许的授权范围
.autoApprove(false)//false跳转到授权页面
//客户端接收授权码的重定向地址
.redirectUris("http://ip:8100/api/user");
/*
1、get请求获取授权码
地址: /oauth/authorize?client_id=XcWebApp&response_type=code&scope=all&redirect_uri=http://ip:8100/api/user
参数列表如下:
•client_id:客户端准入标识。
•response_type:授权码模式固定为code。
•scope:客户端权限。
•redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。
2、请求成功,重定向至http://www.xuecheng-plus.com/?code=授权码,比如:http://www.xuecheng-plus.com/?code=Wqjb5H
3、使用httpclient工具post申请令牌
/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=authorization_code&code=授权码&redirect_uri=http://ip:8100/api/user
参数列表如下
•client_id:客户端准入标识。
•client_secret:客户端秘钥。
•grant_type:授权类型,填写authorization_code,表示授权码模式
•code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
•redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
4、申请令牌成功如下所示:
JSON
{
"access_token": "368b1ee7-a9ee-4e9a-aae6-0fcab243aad2",
"token_type": "bearer",
"refresh_token": "3d56e139-0ee6-4ace-8cbe-1311dfaa991f",
"expires_in": 7199,
"scope": "all"
}
说明:
1、access_token,访问令牌,用于访问资源使用。
2、token_type,bearer是在RFC6750中定义的一种token类型,在携带令牌访问资源时需要在head中加入bearer 空格 令牌内容
3、refresh_token,当令牌快过期时使用刷新令牌可以再次生成令牌。
4、expires_in:过期时间(秒)
5、scope,令牌的权限范围,服务端可以根据令牌的权限范围去对令牌授权。
*/
}
//令牌端点的访问配置
/**
* 用来配置令牌(token)的访问端点和令牌服务(token services)。
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)//认证管理器
.tokenServices(authorizationServerTokenServices)//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
//令牌端点的安全配置
/**
* 用来配置令牌端点的安全约束.
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security){
security
.tokenKeyAccess("permitAll()") //oauth/token_key是公开
.checkTokenAccess("permitAll()") //oauth/check_token公开
.allowFormAuthenticationForClients() //表单认证(申请令牌)
;
}
}
SecurityConfig :
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//OAUTH2.0 配置认证管理bean
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// //配置用户信息服务 这里注释之后需要实现一个接口--UserDetailsService
// @Bean
// public UserDetailsService userDetailsService() {
// //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
// InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
// manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
// return manager;
// }
@Bean
public PasswordEncoder passwordEncoder() {
//密码为明文方式
// return NoOpPasswordEncoder.getInstance();
//密码加密
return new BCryptPasswordEncoder();
}
//配置安全拦截机制
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过
.anyRequest().permitAll()//其它请求全部放行
.and()
.formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-success
http.logout().logoutUrl("/logout");//退出地址
}
}
TokenConfig:
/**
* 令牌策略配置类
* @author Administrator
* @version 1.0
**/
@Configuration
public class TokenConfig {
private String SIGNING_KEY = "mq123";
@Autowired
TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
//令牌管理服务
@Bean(name="authorizationServerTokenServicesCustom")
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service=new DefaultTokenServices();
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存储策略
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
}
LoginController:
/**
* @author Mr.M
* @version 1.0
* @description 测试controller
* @date 2022/9/27 17:25
*/
@Slf4j
@RestController
public class LoginController {
@Autowired
private UserService userService;
@RequestMapping("/login-success")
public String loginSuccess(){
return "登录成功";
}
@RequestMapping("/user/{id}")
public Result<User> getuser(@PathVariable("id") Integer id){
return userService.getUserByIdWithCache(id);
}
@RequestMapping("/r/r1")
@PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问
public String r1(){
return "访问r1资源";
}
@RequestMapping("/r/r2")
@PreAuthorize("hasAuthority('p2')")//拥有p1权限方可访问
public String r2(){
return "访问r2资源";
}
}
LoginUserServiceImpl :
/**
* @author Mr.M
* @version 1.0
* @description TODO
* @date 2022/9/28 18:09
*/
@Service
@Slf4j
public class LoginUserServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
/**
* @description 根据账号查询用户信息
* @param s 账号
* @return org.springframework.security.core.userdetails.UserDetails
* @author Mr.M
* @date 2022/9/28 18:30
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
final QueryWrapper<User> objectQueryWrapper = new QueryWrapper<>();
objectQueryWrapper.eq("username",s);
final User user = userMapper.selectOne(objectQueryWrapper);
if(user==null){
//返回空表示用户不存在
return null;
}
log.info("UserServiceImpl:{}" ,user.toString());
//取出数据库存储的正确密码
String password =user.getPassword();
//用户权限,如果不加报Cannot pass a null GrantedAuthority collection
String[] authorities= {"p1"};
//为了安全在令牌中不放密码
user.setPassword(null);
//将user对象转json
String userString = JSON.toJSONString(user);
//创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入
UserDetails userDetails = org.springframework.security.core.userdetails.User
.withUsername(userString).password(password).authorities(authorities).build();
return userDetails;
}
public static void main(String[] args) {
String password = "1234";
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
for(int i=0;i<10;i++) {
//每个计算出的Hash值都不一样
String hashPass = passwordEncoder.encode(password);
System.out.println(hashPass);
//虽然每次计算的密码Hash值不一样但是校验是通过的
boolean f = passwordEncoder.matches(password, hashPass);
System.out.println(f);
}
}
}
LoginUserServiceImpl 实现了 UserDetailsService 之后,在security登录的时候,就会根据LoginUserServiceImpl 中的loadUserByUsername对登录的用户的账号密码进行校验。
然后将用户信息加入org.springframework.security.core.userdetails.User中,由于上述代码的配置,使用的是jwt的配置。
使用密码模式,可以获取到jwt的token信息如下图所示:
http://localhost:8101/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=zhangsan&password=123
在access_token
中,就会包含用户的信息。(注意token中不要存放密码等敏感信息)
由于其他服务也需要收到security的保护,所以需要进行一定的配置。
<!--认证相关-->
<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>
<!-- nacos客户端依赖包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<!--<!– nacos的配置管理依赖 –>-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
ResouceServerConfig :
/**
* 资源服务配置
* @author Administrator
* @version 1.0
**/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
//资源服务标识,需要和认证服务的标识符保持一致
public static final String RESOURCE_ID = "xuecheng-plus";
@Autowired
TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)//资源 id
.tokenStore(tokenStore)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/r/**","/course/**","/api/**").authenticated()//所有/api/**的请求必须认证通过
//根据配置可知/course/**开头的接口需要认证通过。
.anyRequest().permitAll()
;
}
}
TokenConfig :
/**
* 令牌策略配置类
* @author Administrator
* @version 1.0
**/
@Configuration
public class TokenConfig {
//jwt签名密钥,与认证服务保持一致,需要相同的密钥才能成功解析token
private String SIGNING_KEY = "mq123";
@Bean
public TokenStore tokenStore() {
//JWT令牌存储方案
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
/* @Bean
public TokenStore tokenStore() {
//使用内存存储令牌(普通令牌)
return new InMemoryTokenStore();
}*/
}
配置完成之后,请求该服务的接口时,需要带上token才能访问。
<!-- nacos客户端依赖包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<!--<!– nacos的配置管理依赖 –>-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</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.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
GatewayAuthFilter :
@Component
@Slf4j
public class GatewayAuthFilter implements GlobalFilter, Ordered {
//白名单
private static List<String> whitelist = null;
static {
//加载白名单
try (
InputStream resourceAsStream = GatewayAuthFilter.class.getResourceAsStream("/security-whitelist.properties");
) {
Properties properties = new Properties();
properties.load(resourceAsStream);
Set<String> strings = properties.stringPropertyNames();
System.out.println(strings.toString());
whitelist= new ArrayList<>(strings);
} catch (Exception e) {
log.error("加载/security-whitelist.properties出错:{}",e.getMessage());
e.printStackTrace();
}
}
@Autowired
private TokenStore tokenStore;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestUrl = exchange.getRequest().getPath().value();
AntPathMatcher pathMatcher = new AntPathMatcher();//用于做路径判断
//白名单放行 白名单不需要jwttoken
for (String url : whitelist) {
if (pathMatcher.match(url, requestUrl)) {
return chain.filter(exchange);
}
}
//检查token是否存在
String token = getToken(exchange);
if (!StringUtils.isNotEmpty(token)) {
return buildReturnMono("没有认证",exchange);
}
//判断是否是有效的token
OAuth2AccessToken oAuth2AccessToken;
try {
oAuth2AccessToken = tokenStore.readAccessToken(token);
boolean expired = oAuth2AccessToken.isExpired();
if (expired) {
return buildReturnMono("认证令牌已过期",exchange);
}
return chain.filter(exchange);
} catch (InvalidTokenException e) {
log.info("认证令牌无效: {}", token);
return buildReturnMono("认证令牌无效",exchange);
}
}
/**
* 获取token
*/
private String getToken(ServerWebExchange exchange) {
String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
if (!StringUtils.isNotEmpty(tokenStr)) {
return null;
}
String token = tokenStr.split(" ")[1];
if (!StringUtils.isNotEmpty(token)) {
return null;
}
return token;
}
//返回响应结果
private Mono<Void> buildReturnMono(String error, ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
String jsonString = JSON.toJSONString(new RestErrorResponse(error));
byte[] bits = jsonString.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return 0;
}
}
RestErrorResponse :
/**
* 错误响应参数包装
*/
public class RestErrorResponse implements Serializable {
private String errMessage;
public RestErrorResponse(String errMessage){
this.errMessage= errMessage;
}
public String getErrMessage() {
return errMessage;
}
public void setErrMessage(String errMessage) {
this.errMessage = errMessage;
}
}
SecurityConfig :
/**
* @description 安全配置类
* @author Mr.M
* @date 2022/9/27 12:07
* @version 1.0
*/
@EnableWebFluxSecurity
@Configuration
public class SecurityConfig {
//安全拦截配置
@Bean
public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
return http.authorizeExchange()
.pathMatchers("/**").permitAll()
.anyExchange().authenticated()
.and().csrf().disable().build();
}
}
TokenConfig :
/**
* @author Administrator
* @version 1.0
**/
@Configuration
public class TokenConfig {
String SIGNING_KEY = "mq123";
// @Bean
// public TokenStore tokenStore() {
// //使用内存存储令牌(普通令牌)
// return new InMemoryTokenStore();
// }
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}