一、介绍
Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是 SpringSecurity 重要核心功能。
Spring Security进行认证和鉴权的时候,就是利用的一系列的Filter来进行拦截的。
如图所示,一个请求想要访问到API就会从左到右经过虚线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分是负责异常处理,橙色部分则是负责授权。进过一系列拦截最终访问到我们的API。这里面重点关注两个过滤器:UsernamePasswordAuthenticationFilter负责登录认证,FilterSecurityInterceptor负责权限授权。
二、SpringSecurity入门
1、在项目中添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、启动项目测试
在浏览器访问:http://localhost:8080/doc.html,自动跳转到了登录页面。
默认的用户名:user,密码在项目启动的时候在控制台会打印,注意每次启动的时候密码都会发生变化。
当输入错误的帐号密码时,页面会出现提示。
如果正确的话,则能正常访问:
说明Spring Security默认安全保护生效。在实际开发中,这些默认的配置是不能满足我们需要的,我们需要扩展Spring Security组件,完成自定义配置,实现我们的项目需求。
三、用户认证
用户认证流程:
- Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
- AuthenticationManager接口:定义了认证Authentication的方法
- UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
- UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
1、用户认证核心组件
我们系统中会有许多用户,确认当前是哪个用户正在使用我们系统就是登录认证的最终目的。在Spring Security中的体现就是 Authentication,它存储了认证信息,代表当前登录用户。我们在程序中如何获取并使用它呢?我们需要通过 SecurityContext 来获取Authentication,SecurityContext就是我们的上下文对象。这个上下文对象则是交由 SecurityContextHolder 进行管理,你可以在程序任何地方使用它:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Authentication
中的信息:
Principal
:用户信息,没有认证时一般是用户名,认证后一般是用户对象Credentials
:用户凭证,一般是密码Authorities
:用户权限
2、用户认证
AuthenticationManager 就是Spring Security用于执行身份验证的组件,只需要调用它的authenticate方法即可完成认证。Spring Security默认的认证方式就是在UsernamePasswordAuthenticationFilter这个过滤器中进行认证的,该过滤器负责认证逻辑。Spring Security用户认证关键代码如下:
// 生成一个包含账号密码的认证信息
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(username, passwrod);
// AuthenticationManager校验这个认证信息,返回一个已认证的Authentication
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 将返回的Authentication存到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
AuthenticationManager的校验逻辑非常简单:根据用户名先查询出用户对象(没有查到则抛出异常)将用户对象的密码和传递过来的密码进行校验,密码不匹配则抛出异常。其中使用了三个组件:
- UserDetialsService接口只有一个方法loadUserByUsername(String username),通过用户名查询用户对象,默认实现是在内存中查询。
- Spring Security中的用户数据则是由UserDetails来体现,该接口中提供了账号、密码等通用属性。
- PasswordEncoder负责密码加密与校验。
UserDetialsService、UserDetails、PasswordEncoder,这三个组件Spring Security都有默认实现,这一般是满足不了我们的实际需求的,所以这里我们自己来实现这些组件。
加密器PasswordEncoder
package com.ywz.security;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.DigestUtils;
import java.util.Arrays;
/**
* 类描述 -> 自定义md5加密
*
* @Author: ywz
* @Date: 2024/07/28
*/
public class CustomMd5PasswordEncoder implements PasswordEncoder {
/**
* 方法描述 -> 对密码进行md5加密
*
* @param rawPassword 未加密密码
* @Return: @return {@link String }
* @Author: ywz
* @Date: 2024/07/28
*/
@Override
public String encode(CharSequence rawPassword) {
return Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes()));
}
/**
* 方法描述 -> 判断密码是否匹配
*
* @param rawPassword 未加密密码
* @param encodedPassword 加密后的密码
* @Return: @return boolean
* @Author: ywz
* @Date: 2024/07/28
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes())));
}
}
用户对象UserDetails
该接口是用户对象,它提供了用户的一些通用属性,实际开发中我们的用户属性各种各样,这些默认属性可能是满足不了,所以我们一般会自己实现该接口,然后设置好我们实际的用户实体对象。实现此接口要重写很多方法比较麻烦,我们可以继承Spring Security提供的org.springframework.security.core.userdetails.User类,该类实现了UserDetails接口帮我们省去了重写方法的工作:
package com.ywz.security;
import com.ywz.pojo.SysUser;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
/**
* 类描述 -> 自定义用户
*
* @Author: ywz
* @Date: 2024/07/28
*/
@Setter
@Getter
public class CustomUser extends User {
private SysUser sysUser;
public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
super(sysUser.getUsername(), sysUser.getPassword(), authorities);
this.sysUser = sysUser;
}
}
业务对象UserDetailsService
该接口很简单只有一个方法,我们实现该接口,就完成了自己的业务:
package com.ywz.security.service;
import com.ywz.pojo.SysUser;
import com.ywz.security.CustomUser;
import com.ywz.service.SysUserService;
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 javax.annotation.Resource;
import java.util.Collections;
import java.util.Objects;
/**
* 类描述 -> 实现UserDetailsService接口,重写方法
*
* @Author: ywz
* @Date: 2024/07/28
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService{
@Resource
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.queryByUsername(username);
if (Objects.isNull(sysUser)){
throw new UsernameNotFoundException("用户名不存在!");
}
if(sysUser.getStatus() == 0) {
throw new RuntimeException("账号已停用");
}
return new CustomUser(sysUser, Collections.emptyList());
}
}
登录接口
接下需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。 认证成功的话要生成一个jwt,放入响应中返回。
package com.ywz.controller;
import com.ywz.pojo.LoginVo;
import com.ywz.pojo.Result;
import com.ywz.service.SysUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Map;
/**
* 类描述 -> 登录控制器
*
* @Author: ywz
* @Date: 2024/07/28
*/
@Api(tags = "系统管理-登录管理")
@RequestMapping("/admin/system/index")
@RestController
public class LoginController {
@Resource
private SysUserService sysUserService;
/**
* 方法描述 -> 登录接口
*
* @param loginVo -> 登录对象
* @Return: @return {@link Result }<{@link Map }<{@link String },{@link Object }>>
* @Author: ywz
* @Date: 2024/07/28
*/
@ApiOperation("登录接口")
@PostMapping("/login")
public Result<Map<String,Object>> login(@RequestBody LoginVo loginVo){
return sysUserService.login(loginVo);
}
}
SecurityConfig配置
package com.ywz.security;
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.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Collections;
/**
* 类描述 -> SpringSecurity配置类
*
* @Author: ywz
* @Date: 2024/07/28
*/
@Configuration
@EnableWebSecurity // 是开启SpringSecurity的默认行为
public class SecurityConfig {
/**
* 方法描述 -> 密码明文加密方式配置
*
* @Return: @return {@link PasswordEncoder }
* @Author: ywz
* @Date: 2024/07/28
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new CustomMd5PasswordEncoder();
}
/**
* 方法描述 -> 获取AuthenticationManager(认证管理器),登录时认证使用
*
* @param authenticationConfiguration 认证配置
* @Return: @return {@link AuthenticationManager }
* @Author: ywz
* @Date: 2024/07/28
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
/**
* 方法描述 -> 获取SecurityFilterChain(过滤器链),配置过滤器
*
* @param http httpSecurity
* @Return: @return {@link SecurityFilterChain }
* @Author: ywz
* @Date: 2024/07/28
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// 基于 token,不需要 csrf
.csrf().disable()
// 开启跨域以便前端调用接口
.cors().and()
.authorizeRequests()
// 指定某些接口不需要通过验证即可访问。登录接口肯定是不需要认证的
.antMatchers("/admin/system/index/login").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**","/doc.html").permitAll()
// 这里意思是其它所有接口需要认证才能访问
.anyRequest().authenticated()
.and()
// 基于 token,不需要 session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// cors security 解决方案
.cors().configurationSource(corsConfigurationSource())
.and()
.build();
}
/**
* 方法描述 -> 配置跨源访问(CORS)
*
* @Return: @return {@link CorsConfigurationSource }
* @Author: ywz
* @Date: 2024/07/28
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowedOrigins(Collections.singletonList("*"));
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
执行登录
controller通过login方法调用实际业务
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
@Resource
private SysMenuService sysMenuService;
//通过AuthenticationManager的authenticate方法来进行用户认证,
@Resource
private AuthenticationManager authenticationManager;
@Override
public Result<Map<String, Object>> login(LoginVo loginVo) {
// 将表单数据封装到 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
// authenticate方法会调用loadUserByUsername
Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
if(Objects.isNull(authenticate)){
throw new RuntimeException("用户名或密码错误");
}
// 校验成功,强转对象
CustomUser customUser = (CustomUser) authenticate.getPrincipal();
SysUser sysUser = customUser.getSysUser();
// 校验通过返回token
String token = JwtUtil.createToken(sysUser.getId(), sysUser.getUsername());
Map<String, Object> map = new HashMap<>();
map.put("token",token);
return Result.ok(map);
}
}
认证过滤器
我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的信息,获取对应的LoginUser对象。然后封装Authentication对象存入SecurityContextHolder。
/**
* 类描述 -> Jwt认证过滤器
*
* @Author: ywz
* @Date: 2024/07/28
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
/**
* 方法描述 -> 在请求之前进行过滤
*
* @param request 请求
* @param response 响应
* @param filterChain 过滤器链
* @Return:
* @Author: ywz
* @Date: 2024/07/28
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(request, response);
return;
}
//解析token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}