Spring Security使用教程
环境搭建
-
创建一个SpringBoot项目
-
引入SpringSecrity依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
快速入门
认证
- 自定义自己的实现类,替换SpringSecurity默认的实现类
自定义UserDetailsService
@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
/**
*
* @param s 用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//1.到数据库查询用户信息
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(User::getUserName, s);
User user = userMapper.selectOne(lambdaQueryWrapper);
if (Objects.isNull(user)) {
throw new RuntimeException("用户名不存在");
}
//2.封装用户信息为UserDetails
LoginUserDetails loginUserDetails = new LoginUserDetails(user);
return loginUserDetails;
}
}
自定义UserDetails
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUserDetails implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
替换默认密码加密和校验实现类
@RequiredArgsConstructor
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
*修改SpringSecurity的默认密码校验方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 注入一个AuthenticationManager
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置放行登陆接口,配置自定义认证的过滤器
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭csrf
http = http.csrf().disable();
//不通过session获取SecurityContext
http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
//匿名访问,未认证可以访问,认证后不可以访问
http = http.authorizeRequests().antMatchers("/user/login").anonymous()
//其他接口需要认证才能访问
.anyRequest().authenticated().and();
//配置自定义认证过滤器
http = http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//认证后才能访问
//http.authorizeRequests().antMatchers("").authenticated();
//无论有没有认证都可以访问
//http.authorizeRequests().antMatchers("").permitAll();
//没有认证才能访问
//http.authorizeRequests().antMatchers("").anonymous();
}
}
自定义登陆接口
- 控制器
@RequiredArgsConstructor
@RestController
public class LoginController {
private final LoginService loginService;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user){
return loginService.login(user);
}
}
- 服务层
- 使用AuthenticationManager进行用户认证
- 认证成功,使用用户id生成jwt
- 将jwt存入redis
- 将jwt返回
@RequiredArgsConstructor
@Service
public class LoginServiceImpl implements LoginService {
private final AuthenticationManager authenticationManager;
private final RedisCache redisCache;
@Override
public ResponseResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
//使用authentication进行用户认证
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//校验是否认证成功
if (Objects.isNull(authenticate)) {
throw new RuntimeException("用户名或密码错误");
}
//使用id生产JWT,JSON Web Token
LoginUserDetails principal = (LoginUserDetails) authenticate.getPrincipal();
String token = JwtUtil.createJWT(principal.getUser().getId().toString());
//将token放到redis中
redisCache.setCacheObject("user:"+principal.getUser().getId(),principal);
Map<String,String> map = new HashMap<>();
map.put("token",token);
return new ResponseResult<Map<String,String>>(200,"登陆成功",map);
}
}
修改Security配置,放行自定义登陆接口
-
常用配置规则
//认证后才能访问 http.authorizeRequests().antMatchers("").authenticated(); //无论有没有认证都可以访问 http.authorizeRequests().antMatchers("").permitAll(); //没有认证才能访问 http.authorizeRequests().antMatchers("").anonymous();
-
添加配置
@RequiredArgsConstructor
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
*修改SpringSecurity的默认密码校验方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 注入一个AuthenticationManager
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置放行登陆接口,配置自定义认证的过滤器
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭csrf
http = http.csrf().disable();
//不通过session获取SecurityContext
http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
//匿名访问,未认证可以访问,认证后不可以访问
http = http.authorizeRequests().antMatchers("/user/login").anonymous()
//其他接口需要认证才能访问
.anyRequest().authenticated().and();
//配置自定义认证过滤器
http = http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//认证后才能访问
//http.authorizeRequests().antMatchers("").authenticated();
//无论有没有认证都可以访问
//http.authorizeRequests().antMatchers("").permitAll();
//没有认证才能访问
//http.authorizeRequests().antMatchers("").anonymous();
}
}
自定义退出登陆接口
- 控制器
@RequiredArgsConstructor
@RestController
public class LoginController {
private final LoginService loginService;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user){
return loginService.login(user);
}
@RequestMapping("/user/logout")
public ResponseResult logout(){
return loginService.logout();
}
}
- 服务层
@RequiredArgsConstructor
@Service
public class LoginServiceImpl implements LoginService {
private final AuthenticationManager authenticationManager;
private final RedisCache redisCache;
@Override
public ResponseResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
//使用authentication进行用户认证
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//校验是否认证成功
if (Objects.isNull(authenticate)) {
throw new RuntimeException("用户名或密码错误");
}
//使用id生产JWT,JSON Web Token
LoginUserDetails principal = (LoginUserDetails) authenticate.getPrincipal();
String token = JwtUtil.createJWT(principal.getUser().getId().toString());
//将token放到redis中
redisCache.setCacheObject("user:"+principal.getUser().getId(),principal);
Map<String,String> map = new HashMap<>();
map.put("token",token);
return new ResponseResult<Map<String,String>>(200,"登陆成功",map);
}
@Override
public ResponseResult logout() {
//从SecurityContext中获取用户id
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUserDetails userDetails = (LoginUserDetails) authentication.getPrincipal();
Long id = userDetails.getUser().getId();
//删除redis中的数据
redisCache.deleteObject("user:"+id.toString());
//清除SecurityContext中的数据
SecurityContextHolder.clearContext();
return new ResponseResult(200,"退出成功");
}
}
自定义认证过滤器
- 继承OncePerRequestFilter,确保一次请求只经过一次该过滤器
- 从请求头中获取token
- 解析token获取用户d
- 根据用户id从redis中获取用户信息
- 将获取到的用户信息封装到UsernamePasswordAuthenticationToken中,存入SecurityContext中
@RequiredArgsConstructor
@Component
public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
private final RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//从请求头中获取token
String token = httpServletRequest.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
//不执行过滤器链后续的操作
return;
}
//解析token获取用户id
String id;
try {
Claims claims = JwtUtil.parseJWT(token);
id = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis获取用户信息
LoginUserDetails user = redisCache.getCacheObject("user:" + id);
if (Objects.isNull(user)) {
throw new RuntimeException("未登录");
}
//将用户信息放入SecurityContext中
//这里使用三个参数的构造器创建authentication对象,SpringSecurity会讲当前用户设置为已认证状态
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
在Security过滤器链中添加自定义认证过滤器
- 在配置类中添加
//配置自定义认证过滤器
http = http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
授权
给接口添加访问权限限制
- 在Security配置类中加上注解@EnableGlobalMethodSecurity,启用访问限制
@EnableGlobalMethodSecurity(prePostEnabled = true)
- 在接口添加访问需要的权限,
@PreAuthorize("hasAuthority('test')")
@RestController
public class HelloController {
@PreAuthorize("hasAuthority('test')")
@RequestMapping("/hello")
public String hello() {
return "hello";
}
}
在用户认证时,查询到用户权限信息,封装到LoginDetails对象中
@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
/**
*
* @param s 用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//1.到数据库查询用户信息
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(User::getUserName, s);
User user = userMapper.selectOne(lambdaQueryWrapper);
if (Objects.isNull(user)) {
throw new RuntimeException("用户名不存在");
}
//2.封装用户信息为UserDetails
//权限信息
List<String> authorities = Arrays.asList("test","admin");
LoginUserDetails loginUserDetails = new LoginUserDetails(user,authorities);
return loginUserDetails;
}
}
- UserDetails
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUserDetails implements UserDetails {
private User user;
private List<String> permissions;
public LoginUserDetails(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
//忽略序列化
/**
* 注意需要将该字段设置为false,否则会报错
* 且必须有一个属性authorities,否则序列化时会将SimpleGrantedAuthority进行序列化导致反序列化时异常
*/
@JSONField(serialize = false)
private transient List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities!=null) {
return authorities;
}
//将权限信息转换为GrantedAuthority
authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
在认证过滤器中查询到用户权限信息保存到SecurityContext中保存
- 关键代码
//将用户信息和用户权限封装到authentication
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
- 完整代码
@RequiredArgsConstructor
@Component
public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
private final RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//从请求头中获取token
String token = httpServletRequest.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
//不执行过滤器链后续的操作
return;
}
//解析token获取用户id
String id;
try {
Claims claims = JwtUtil.parseJWT(token);
id = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis获取用户信息
LoginUserDetails user = redisCache.getCacheObject("user:" + id);
if (Objects.isNull(user)) {
throw new RuntimeException("未登录");
}
//将用户信息放入SecurityContext中
//这里使用三个参数的构造器创建authentication对象,SpringSecurity会讲当前用户设置为已认证状态
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
自定义异常处理
- 在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到
- 在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常
- 如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可
自定义认证异常处理
- 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//创建响应对象
ResponseResult<String> result = new ResponseResult<>(HttpStatus.UNAUTHORIZED.value(),"认证失败"+ e.getMessage());
//转换为JSON字符串
String json = JSON.toJSONString(result);
//封装响应
WebUtils.renderString(httpServletResponse,json);
}
}
自定义授权异常处理
- 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
//创建响应对象
ResponseResult<String> result = new ResponseResult<>(HttpStatus.UNAUTHORIZED.value(),"权限不足"+ e.getMessage());
//转换为JSON字符串
String json = JSON.toJSONString(result);
//封装响应
WebUtils.renderString(httpServletResponse,json);
}
}
配置自定义异常处理器
- 核心部分
//配置自定义认证异常处理器
http = http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and();
//配置自定义授权异常处理器
http = http.exceptionHandling().accessDeniedHandler(accessDeniedHandler).and();
- 完整代码
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;
private final AuthenticationEntryPoint authenticationEntryPoint;
private final AccessDeniedHandler accessDeniedHandler;
/**
*修改SpringSecurity的默认密码校验方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 注入一个AuthenticationManager
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置放行登陆接口,配置自定义认证的过滤器
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭csrf
http = http.csrf().disable();
//不通过session获取SecurityContext
http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
//匿名访问,未认证可以访问,认证后不可以访问
http = http.authorizeRequests().antMatchers("/user/login").anonymous()
//其他接口需要认证才能访问
.anyRequest().authenticated().and();
//配置自定义认证过滤器
http = http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//认证后才能访问
//http.authorizeRequests().antMatchers("").authenticated();
//无论有没有认证都可以访问
//http.authorizeRequests().antMatchers("").permitAll();
//没有认证才能访问
//http.authorizeRequests().antMatchers("").anonymous();
//配置自定义异常处理器
//配置自定义认证异常处理器
http = http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and();
//配置自定义授权异常处理器
http = http.exceptionHandling().accessDeniedHandler(accessDeniedHandler).and();
}
}
解决SpringSecurity中的跨域问题
在SpringBoot中允许跨域访问
- 通过是实现WebMvcConfigurer的方式配置允许跨域
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry
//允许跨域的url
.addMapping("/**")
//允许所有源访问,允许跨域
.allowedOriginPatterns("*")
//允许使用cookie
.allowCredentials(true)
//允许的请求方式
.allowedMethods("GET","POST","PUT","DELETE")
//允许设置请求头
.allowedHeaders("*")
//允许跨域的时间
.maxAge(3600);
}
}
在SpringSecurity中允许跨域访问
- 核心代码
//配置允许跨域
http.cors();
- 完整代码
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;
private final AuthenticationEntryPoint authenticationEntryPoint;
private final AccessDeniedHandler accessDeniedHandler;
/**
*修改SpringSecurity的默认密码校验方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 注入一个AuthenticationManager
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置放行登陆接口,配置自定义认证的过滤器
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭csrf
http = http.csrf().disable();
//不通过session获取SecurityContext
http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
//匿名访问,未认证可以访问,认证后不可以访问
http = http.authorizeRequests().antMatchers("/user/login").anonymous()
//其他接口需要认证才能访问
.anyRequest().authenticated().and();
//配置自定义认证过滤器
http = http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//认证后才能访问
//http.authorizeRequests().antMatchers("").authenticated();
//无论有没有认证都可以访问
//http.authorizeRequests().antMatchers("").permitAll();
//没有认证才能访问
//http.authorizeRequests().antMatchers("").anonymous();
//配置自定义异常处理器
//配置自定义认证异常处理器
http = http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and();
//配置自定义授权异常处理器
http = http.exceptionHandling().accessDeniedHandler(accessDeniedHandler).and();
//配置允许跨域
http.cors();
}
}
自定义权限校验方法
- 使用SPEL表达式获取spring容器中bean,然后调用bean对应的方法进行校验
- 使用
@beanName
的方式可以获取spring容器中的bean - 使用
#{beanName.age}
的方式可以在xml配置文件中使用spring容器bean的属性和方法
自定义权限校验
- 自定义权限校验逻辑,并注入spring容器
@Component("myAuthentication")
public class MyAuthenticationHandler {
public boolean hasAuthority(String authority){
//获取用户具有的权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUserDetails user = (LoginUserDetails) authentication.getPrincipal();
List<String> permissions = user.getPermissions();
//判断用户是否具有访问的权限
return permissions.contains(authority);
}
}
在接口使用自定义的权限校验方法
- 使用SPEL表达式的
@
方式获取spring容器中的bean,调用bean的方式进行权限校验 @myAuthentication.hasAuthority('system:dept:list')
表示获取spring容器中名称为myAuthentication的bean,然后调用hasAuthority方法进行权限校验- 校验流程与SpringSecurity默认流程类型,只需要校验方法返回一个布尔值即可
@RestController
public class HelloController {
@PreAuthorize("hasAuthority('system:dept:list')")
@RequestMapping("/hello")
public String hello() {
return "hello";
}
/**
* 使用自定义权限校验方法
* @return
*/
@PreAuthorize("@myAuthentication.hasAuthority('system:dept:list')")
@RequestMapping("/testMyAuthenticationHandler")
public ResponseResult testMyAuthenticationHandler(){
return new ResponseResult(HttpStatus.OK.value(),"校验通过");
}
}