简述SpringSecurity:
SpringSecurity
的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。基本过滤器链如下图所示:
图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor:负责权限校验的过滤器。
具体的认证流程图:
具体实现:一般Web应用的需要进行认证和授权。
认证思路:
一般我们会修改UsernamePasswordAuthenticationFliter和 InMemoryUserDetailsManager两个过滤器。
SpringSecurity
在默认的认证过程中如果账号密码校验成功会返回Authentication对象,之后UsernamePasswordAuthenticationFilter
会将用户信息Authentication
存入SecurityContextHolder
中,但是我们在实际运用场景中认证通过后还需要向前端返回一个JSON格式的数据里面包括了JWT,所以此时我们需要写一个自定义登录接口来替代UsernamePasswordAuthenticationFliter的功能,用我们自己写的接口去调用调用ProviderManager
的方法进行认证。
InMemoryUserDetailsManager作用就是帮我们进行用户认证,我们可以重写UserDetailsService类中的方法去修改认证方式。
实现:
引入Maven依赖:
<!-- SpringSecurity-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
修改InMemoryUserDetailsManager认证方式(重写UserDetailsService类中的方法):
SpringSecurity
默认是在内存中查找对应的用户名密码然后UserDetailsService
的默认实现类使用封装成UserDetai
l对象交给DaoAuthenticationProcider
校验
但是我们在实际运用场景中是从数据库中查找用户信息
所以此时我们需要写一个UserDetailsService
的实现类用来在数据库中查询用户信息并且封装到UserDetai
l对象中
并且需要写一个UserDetail
的实现类因为用户信息不仅仅只有用户名和密码还有其他信息
package com.swp.springsecurity.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.swp.springsecurity.domain.LoginUser;
import com.swp.springsecurity.domain.User;
import com.swp.springsecurity.mapper.MenuMapper;
import com.swp.springsecurity.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Autowired
MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询用户信息
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<User>().eq(User::getUserName, username);
User user = userMapper.selectOne(wrapper);
//如果没有该用户就抛出异常
if (Objects.isNull(user)) {
throw new RuntimeException("用户名或密码错误");
}
// 查询权限信息封装到LoginUser中
List<String> list = menuMapper.selectPermsByUserId(user.getId());
// 将用户信息封装到UserDetails实现类中
return new LoginUser(user,list);
}
}
我们自定义的UserDetail实现类:
package com.swp.springsecurity.domain;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
private List<String> permissions;
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;
public LoginUser(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities != null)
return authorities;
List<SimpleGrantedAuthority> 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;
}
}
自定义登录登出接口,在接口中调用ProviderManager方法去认证
:
这里附上Service层代码:
package com.swp.springsecurity.service.impl;
import com.swp.springsecurity.domain.LoginUser;
import com.swp.springsecurity.domain.ResponseResult;
import com.swp.springsecurity.domain.User;
import com.swp.springsecurity.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import com.swp.springsecurity.utils.JwtUtil;
import com.swp.springsecurity.utils.RedisCache;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.HashMap;
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisCache redisCache;
@Override
public ResponseResult login(User user) {
//1.封装Authentication对象
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
//2.通过AuthenticationManager的authenticate方法来进行用户认证
Authentication authenticated = authenticationManager.authenticate(authenticationToken);
//3.在Authentication中获取用户信息
LoginUser loginUser = (LoginUser) authenticated.getPrincipal();
String userId = loginUser.getUser().getId().toString();
//4.认证通过生成token
String jwt = JwtUtil.createJWT(userId);
//5.用户信息存入redis
redisCache.setCacheObject("login:" + userId, loginUser);
//6.把token返回给前端
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("token", jwt);
return new ResponseResult(200, "登录成功", hashMap);
}
@Override
public ResponseResult logout() {
//获取SecurityContextHolder中的用户id
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userId = loginUser.getUser().getId();
//删除redis中的用户信息
redisCache.deleteObject("login:" + userId);
return new ResponseResult(200, "退出成功");
}
}
注意要让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问,我们新建一个SpringSecurity的配置类:
package com.swp.springsecurity.config;
import com.swp.springsecurity.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig{
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
AuthenticationConfiguration authenticationConfiguration;
// 实际项目中我们不会把密码明文存储在数据库中。默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
/**
* 配置Spring Security的过滤链。
*
* @param http 用于构建安全配置的HttpSecurity对象。
* @return 返回配置好的SecurityFilterChain对象。
* @throws Exception 如果配置过程中发生错误,则抛出异常。
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF保护
.csrf(AbstractHttpConfigurer::disable)
// 设置会话创建策略为无状态
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 配置授权规则 指定user/login路径.允许匿名访问(未登录可访问已登陆不能访问). 其他路径需要身份认证
.authorizeHttpRequests(auth -> auth.requestMatchers(new AntPathRequestMatcher("/user/login")).permitAll().anyRequest().authenticated())
//开启跨域访问
.cors()
.and()
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 构建并返回安全过滤链
return http.build();
}
}
授权思路:
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication,然后设置我们的资源所需要的权限即可。
实现:
启动类上开启SpringSecurity权限控制:
@EnableGlobalMethodSecurity(prePostEnabled = true)
在我们想要进行权限控制的类上加@PreAuthorize(“可以自定义返回值boolean的方法”),这里用的是SpringSecurity提供的方法hasAuthority():
// 注意字符串里面还有字符串要用单引号
@PreAuthorize("hasAuthority('test')")
package com.swp.springsecurity.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/hello")
@PreAuthorize("hasAuthority('test')")
public String hello(){
return "World Hello";
}
}
这里将对应的权限信息在UserDetailService环节就从数据库查询并存入了:
权限实际上就是一个个字符串:
// 查询权限信息封装到LoginUser中
List<String> list = menuMapper.selectPermsByUserId(user.getId());
package com.swp.springsecurity.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.swp.springsecurity.domain.LoginUser;
import com.swp.springsecurity.domain.User;
import com.swp.springsecurity.mapper.MenuMapper;
import com.swp.springsecurity.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Autowired
MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询用户信息
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<User>().eq(User::getUserName, username);
User user = userMapper.selectOne(wrapper);
//如果没有该用户就抛出异常
if (Objects.isNull(user)) {
throw new RuntimeException("用户名或密码错误");
}
// 查询权限信息封装到LoginUser中
List<String> list = menuMapper.selectPermsByUserId(user.getId());
// 将用户信息封装到UserDetails实现类中
return new LoginUser(user,list);
}
}
上述就是SpringSecurity的基本使用了,具体的可以查看B站三更老师对应网课,附上大佬的详细代码实现:README.md · Liu332256/SpringSecurity从入门到精通 - Gitee.com