认证,就是判断这个用户身份合法不合法;授权,就是看当前用户有没有访问某一资源的权限。
1.导入认证模块:
拿到一个纯净的springboot工程,基于spring cloud架构,要集成spring security,必须严格按照以下步骤:
1.加入security和oath2两个依赖
只要加入依赖,这个项目就被security管控了。
在auth包下的controller包下定义一个LoginController接口用来测试:
@Slf4j
@RestController
public class LoginController {
@Autowired
XcUserMapper userMapper;
@RequestMapping("/login-success")
public String loginSuccess() {
return "登录成功";
}
@RequestMapping("/user/{id}")
public XcUser getuser(@PathVariable("id") String id) {
XcUser xcUser = userMapper.selectById(id);
return xcUser;
}
@RequestMapping("/r/r1")
@PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问
public String r1() {
return "访问r1资源";
}
@RequestMapping("/r/r2")
@PreAuthorize("hasAuthority('p2')")//拥有p2权限方可访问
public String r2() {
return "访问r2资源";
}
}
我们在数据库找出一个用户id,使用上述代码中的getuser方法的路径,结果弹出一个登录页面,这就是框架自动生成的。这就是认证。
怎么实现授权?
框架管控你的系统,也会管控你的账号。
@PreAuthorize("hasAuthority('p1')")
加了这个注解,框架就会检查访问该方法的账号是否拥有p1权限。
2.在auth模块的config包下定义一个配置类WebSecurityConfig:
/**
* 安全管理配置
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProviderCustom);
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/*
//配置用户信息服务
@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
}
}
这段代码是一个Spring Security配置类,定义了应用程序的安全管理配置。它通过继承 `WebSecurityConfigurerAdapter` 类,并使用注解和方法覆盖来实现安全配置。下面是对每个部分的详细解释:
@EnableWebSecurity
启用Spring Security的Web安全支持。
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
启用方法级别的安全性注解,如 `@Secured` 和 `@PreAuthorize`。
@Autowired
DaoAuthenticationProviderCustom daoAuthenticationProviderCustom
自动注入自定义的认证提供者。
在auth工程的config包下定义了一个自定义的认证提供者类:
@Slf4j
@Component
/**
* 自定义DaoAuthenticationProvider
* 重新了校验密码的方法,因为我们统一了认证的入口,有一些认证方式不需要校验密码
*/
public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {
@Autowired
public void setUserDetailsService(UserDetailsService userDetailsService) {
super.setUserDetailsService(userDetailsService);
}
//屏蔽密码对比
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) {
}
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProviderCustom);
}
配置自定义的认证提供者 `daoAuthenticationProviderCustom`。
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
配置身份验证认证管理器bean,以便其他组件可以自动注入。
@Bean
public PasswordEncoder passwordEncoder() {
/*
//密码为明文方式
return NoOpPasswordEncoder.getInstance();
*/
//密码为加密方式
return new BCryptPasswordEncoder();
}
配置一个密码编码器bean,测试时使用明文,后续会改为使用加密方式。数据库中的密码加过密的,用户输入的密码是明文,数据库中保存的是加密之后的。
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/r/**").authenticated() // 访问/r/** 的请求需要认证
.anyRequest().permitAll() // 其他请求允许所有用户访问
.and()
.formLogin().successForwardUrl("/login-success"); // 登录成功跳转到/login-success
}
配置http安全
http.authorizeRequests()
定义哪些URL路径应该被保护,哪些不需要保护。
.antMatchers("/r/**").authenticated()
任何以 `/r/` 开头的请求都需要用户认证。
.anyRequest().permitAll()
其他请求允许所有用户访问。
http.formLogin().successForwardUrl("/login-success")
配置表单登录,并在成功登录后跳转到 `/login-success`。
Spring Security框架原理见这篇博客:
想要实现认证授权,除了使用Spring Security框架,还必须使用OAuth2协议,详情见下面这篇博客:
2.实现网关认证
从上面那篇博客回来,我们已经测试通过了在content工程中用jwt令牌实现的认证功能,如下图:
如果我们每个微服务都自己实现认证太高麻烦,所以我们使用网关认证,如下图所示:
网关的职责:
1、网站白名单维护
针对不用认证的URL全部放行。
2、校验jwt的合法性。
在gateway工程的config包下定义配置类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();
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) {
//url
String requestUrl = exchange.getRequest().getPath().value();
AntPathMatcher pathMatcher = new AntPathMatcher();
//白名单放行
for (String url : whitelist) {
if (pathMatcher.match(url, requestUrl)) {
return chain.filter(exchange);
}
}
//检查token是否存在
String token = getToken(exchange);
if (StringUtils.isBlank(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.isBlank(tokenStr)) {
return null;
}
String token = tokenStr.split(" ")[1];
if (StringUtils.isBlank(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;
}
}
这是一个名为 `GatewayAuthFilter` 的过滤器类,实现了 Spring Cloud Gateway 中的 `GlobalFilter` 接口和 `Ordered` 接口,用于进行全局的认证和权限控制。
让我们逐步解释这个过滤器的功能和实现:
1.加载白名单:
在静态代码块中,通过读取 `security-whitelist.properties` 配置文件加载了白名单列表。这个列表中的路径是不需要进行认证的,请求会直接通过。
配置文件在gateway工程的resourses包下。
2.过滤方法 `filter`:
这个方法是 `GlobalFilter` 接口的实现,用于实际的过滤逻辑。在方法中,首先判断请求路径是否在白名单中,如果在白名单中则直接放行,不进行认证。然后,从请求头中获取 Authorization 字段,提取出其中的 Token。接着,通过 `tokenStore` 判断 Token 是否有效,如果 Token 无效或已过期,则返回相应的错误信息,否则放行请求。
3.获取 Token 方法 `getToken`:
用于从请求头中提取出 Token 字符串。
4.构建返回的 Mono 对象方法 `buildReturnMono`:
用于构建返回给客户端的 Mono 对象,返回的是一个包含错误信息的 JSON 格式的响应体。
5.实现 `Ordered` 接口方法 `getOrder`:
这个方法用于指定过滤器的执行顺序,数字越小,优先级越高。在这里,返回了 0,表示这个过滤器是优先级最高的,会最先执行。
这个过滤器的主要作用是进行请求的认证和权限控制,在请求到达网关后,会通过该过滤器进行认证,然后再将请求转发给具体的服务进行处理。
3.实现用户认证:
Spring Security工作原理在之前一篇博客中已经介绍了:
用户提交账号和密码由DaoAuthenticationProvider调用UserDetailsService的loadUserByUsername()方法获取UserDetails用户信息。
然后:
UserDetailsService之前曾在 auth工程config包下的WebSecurityConfig配置类中以硬编码的形式自定义过,现在将那里的注释掉。
现在在auth工程的service包下的impl包下定义实现类UserDetailsServiceImpl然后用@Component注解,使用重写的loadUserByUsername方法从数据库获得而不是直接硬编码再用@Bean注解。
在方法上使用 @Bean
注解,您可以告诉 Spring 在容器中注册一个新的 bean 实例。
@Component
是一个通用的注解,用于指示一个类是 Spring 管理的组件。当 Spring 扫描到带有 @Component
注解的类时,它会自动创建该类的一个实例并将其注册到应用程序的上下文中。
虽然 @Bean
和 @Component
都可以用来创建 bean,但是它们之间的主要区别在于使用场景。@Bean
通常用于配置类中,而 @Component
则更适合用于普通的类。
@Slf4j
@Component
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
XcUserMapper xcUserMapper;
@Autowired
ApplicationContext applicationContext;
@Autowired
XcMenuMapper xcMenuMapper;
/**
* 根据username账号查询用户信息
* @param s 账号
* @return org.springframework.security.core.userdetails.UserDetails
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//定义一个AuthParamsDto类对象,将传入的json转成它
AuthParamsDto authParamsDto = null;
try {
//将认证参数转为AuthParamsDto类型
authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
} catch (Exception e) {
log.info("认证请求不符合项目要求:{}",s);
throw new RuntimeException("认证请求数据格式不对");
}
//认证类型,有密码登录,微信登录等等
String authType = authParamsDto.getAuthType();
//根据认证类型从spring容器取出指定的bean
String beanName = authType+"_authservice";
AuthService authService = applicationContext.getBean(beanName, AuthService.class);
//调用统一的execute方法完成认证
XcUserExDto xcUserExDto = authService.execute(authParamsDto);
//将返回的用户信息xcUserExt封装为UserDetails类型并返回
return getUserPrincipal(xcUserExDto);
}
/**
* 查询用户信息
* 将返回的XcUserExDto类型的用户信息封装为UserDetails类型
* @param user 用户扩展信息模型类 包含了用户权限列表
* @return userDetails
*/
public UserDetails getUserPrincipal(XcUserExDto user){
String[] authorities ={};
//根据用户id查询用户权限
List<XcMenu> xcMenus = xcMenuMapper.selectPermissionByUserId(user.getId());
if(xcMenus.size()>0){
List<String> permissions = new ArrayList<>();
xcMenus.forEach(menu->{
//拿到用户拥有的权限标识符
permissions.add(menu.getCode());
});
//将permissions转成数组,得到一个权限数组
authorities = permissions.toArray(new String[0]);
}
String password = user.getPassword();
//为了安全在令牌中不放密码
user.setPassword(null);
//将user对象转json
String userString = JSON.toJSONString(user);
//创建UserDetails对象
UserDetails userDetails = User.withUsername(userString).password(password ).authorities(authorities).build();
return userDetails;
}
}
这段代码是一个 Spring Security 的用户认证服务实现类,用于根据用户账号(username)进行用户认证和权限获取。下面是代码的主要逻辑:
1. 使用 Lombok 的 `@Slf4j` 注解,简化日志记录器的声明。
2. 声明了一个 `UserDetailsServiceImpl` 类,并标记为 Spring 组件(`@Component`)和 Spring Security 的用户认证服务(`@Service`)。
3. 实现了 `UserDetailsService` 接口,重写了其中的 `loadUserByUsername` 方法,该方法根据传入的账号(username)进行用户认证和权限获取。
4. 在 `loadUserByUsername` 方法中,首先根据传入的认证参数类型获取对应的认证服务 `AuthService`,然后调用统一认证方法 `execute` 完成认证,最终将认证通过的用户信息封装为 `UserDetails` 类型并返回。
统一认证方法 `execute`定义于auth工程的AuthService接口中,使用账号密码认证时定义一个实现类PasswordAuthServiceImpl实现该接口;使用微信扫码认证时再定义一个实现类WxAuthServiceImpl实现该接口。
5. 定义了一个 `getUserPrincipal` 方法,用于将 `XcUserExDto` 类型的用户信息封装为 `UserDetails` 类型,并获取用户的权限信息。
6. 在 `getUserPrincipal` 方法中,根据用户id查询用户的权限信息,将权限信息封装为 `UserDetails` 对象,并返回。
在getUserPrincipal方法中,为何要将用户拓展模型类对象user转成json数据呢?
到此我们基于Spring Security认证流程修改为如下: