spring Security是什么?
Spring Security
是一个强大且高度可定制的身份验证和访问控制框架,用于保护基于Spring的应用程序。它是Spring项目的一部分,旨在为企业级系统提供全面的安全性解决方案。
一个简单的授权和校验流程
检验流程
总流程?
对应依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
在自定义检验的时候,主要就是实现UserDetailsService接口,重写loadUserByUserName方法,在该方法中就是去检验账号和密码的准确性。(一般都是进行数据库的查询校验,默认的密码格式就是 ·{}密码·)
前后端分离项目登录流程
1.在springSecurity中我们的需要设置密文的配置,在项目中大多都是使 BCryptPasswordEncoder类来做密码的加密。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.创建对应的login接口和service
Controller?
@RestController
@RequestMapping("/user")
public class LoginController {
@Autowired
LoginService loginService;
@PostMapping("/login")
public ResponseResult login(@RequestBody User user) {
return loginService.login(user);
}
}
Service
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
RedisCache redisCache;
@Override
public ResponseResult login(User user) {
//authenticationManager authenticate进行用户验证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//执行我们对饮的认证方法,在该方法中会返回LoginUser类型的数据
//如果没有通过通过直接抛异常
if(ObjectUtil.isEmpty(authenticate)) {
throw new RuntimeException("登录失败");
}
//如果成功直接生成token,将其也map返回
LoginUser loginUser = (LoginUser)authenticate.getPrincipal();
String jwt = JwtUtil.createJWT(loginUser.getUser().getId().toString());
Map<String, String> data = new HashMap<>();
data.put("token", jwt);
redisCache.setCacheObject(loginUser.getUser().getId().toString(), user);
//返回token
return new ResponseResult(200, "登录成功", data);
}
}
因为AuthenticationManager默认不在ioc中,我们需要将其配置到ioc中,并且配置对应的校验规则。在里面就包括 无效校验的接口(比如:登录接口)和其他一些前后端分离的配置。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//将AuthenticationManager配置到ioc中
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//配置权限规则,主要就睡要放行登录接口,不然登录接口都会被了解,以及其他不要的前后端分离的配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//由于是前后端分离项目,所以要关闭csrf
.csrf().disable()
//由于是前后端分离项目,所以session是失效的,我们就不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//指定让spring security放行登录接口的规则
.authorizeRequests()
// 对于登录接口 anonymous表示允许匿名访问
.antMatchers("/user/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
}
}
进行测试,校验成功。
前后端分离项目校验流程
1.创建一个校验过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token,token会存在header中
String token = request.getHeader("token");
if(StrUtil.isEmpty(token)) {
//由后续的拦截器进行拦截
filterChain.doFilter(request, response);
//后续会返回回来,需要return,不然会执行下面的语句
return ;
}
//解析token
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
if(StringUtil.isNullOrEmpty(userId)) {
throw new RuntimeException("token解析失败");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
//从redis中获取用户的信息
LoginUser loginUser = redisCache.getCacheObject(userId);
if(ObjectUtil.isEmpty(loginUser)) {
throw new RuntimeException("Redis中没有用户信息");
}
//将数据存储到SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
使用三个参数的UsernamePasswordAuthenticationToken的构造器,该构造器会设置授权成功。
2.将过滤器设置到用户验证过滤器之前
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
//设置加密方式
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//将AuthenticationManager配置到ioc中
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//配置权限规则,主要就睡要放行登录接口,不然登录接口都会被了解,以及其他不要的前后端分离的配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//由于是前后端分离项目,所以要关闭csrf
.csrf().disable()
//由于是前后端分离项目,所以session是失效的,我们就不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//指定让spring security放行登录接口的规则
.authorizeRequests()
// 对于登录接口 anonymous表示允许匿名访问
.antMatchers("/user/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
//将过滤器添加到用户登录处理器之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
进行测试,将成功的token放入header中,进行校验。最终校验成功。
退出登录流程
1.编写退出登录接口
@RestController
@RequestMapping("/user")
public class LoginController {
@Autowired
LoginService loginService;
@RequestMapping("/logout")
public ResponseResult logout() {
return loginService.logout();
}
}
2.编写service实现类,删除redis中用户信息的数据,即可完成退出登录操作。在解析的时候redis中的数据不存在就会直接被拦截。
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
RedisCache redisCache;
@Override
public ResponseResult logout() {
//在进入此接口时会先进行解析,成功之后才会执行logout,此时SecurityContextHolder中是有用户信息的
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
if(ObjectUtil.isEmpty(loginUser)) {
throw new RuntimeException("LoginUser不存在");
}
//把redis中的数据删除之后,下次解析的时候就会直接报错,在解析中我们对redis的数据做了判空的操作
redisCache.deleteObject(loginUser.getUser().getId().toString());
return new ResponseResult(200, "退出登录成功", null);
}
}
进行测试,最终成功。
1.开启授权功能,在对应的security的配置类中添加对应的注解。
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启授权
2.为接口设置对应的权限需求
@RestController
public class baseController {
@RequestMapping("/hello")
//拥有text倾向才能访问
@PreAuthorize("hasAuthority('text')")
public String hello() {
return "hello!";
}
}
3.在用户进行认证的时候查询用户拥有的权限集合,并设置到 authenticationToken中。
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
private List<String> authorities2String;
public LoginUser(User user, List<String> authorities2String) {
this.user = user;
this.authorities2String = authorities2String;
}
@JSONField(serialize = false)
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(CollUtil.isEmpty(authorities)) {
return authorities2String.stream()
.map(item -> new SimpleGrantedAuthority(item))
.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;
}
}
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<com.huang.springsecuritydemo.entity.User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq("user_name", username);
com.huang.springsecuritydemo.entity.User user = userMapper.selectOne(userQueryWrapper);
if(ObjectUtil.isEmpty(user)) {
throw new RuntimeException("用户不存在");
}
//todo 查询并设置对应的权限信息
//模拟查到的权限信息
List<String> data = Arrays.asList("test", "text");
return new LoginUser(user, data);
}
}
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token,token会存在header中
String token = request.getHeader("token");
if(StrUtil.isEmpty(token)) {
//由后续的拦截器进行拦截
filterChain.doFilter(request, response);
//后续会返回回来,需要return,不然会执行下面的语句
return ;
}
//解析token
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
if(StringUtil.isNullOrEmpty(userId)) {
throw new RuntimeException("token解析失败");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
//从redis中获取用户的信息
LoginUser loginUser = redisCache.getCacheObject(userId);
if(ObjectUtil.isEmpty(loginUser)) {
throw new RuntimeException("Redis中没有用户信息");
}
//将数据存储到SecurityContextHolder
//todo 设置对应的权限信息
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
进行测试,授权成功。
RDAB模型例子(基本通用,看进行二次开发)
1.创建五个数据库 用户表,角色表,权限表,用户角色关联表,角色权限关联表。
2.编写SQL语句查询用户的所有权限,并使用 mybatis-plus进行封装为一个函数进行调用。
SELECT
DISTINCT m.`perms`
FROM
sys_user_role ur
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
WHERE
user_id = #{id}
AND r.`status` = 0
AND m.`status` = 0
3.在校验是进行调用,并返回对应的权限集合。
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Autowired
MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<com.huang.springsecuritydemo.entity.User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq("user_name", username);
com.huang.springsecuritydemo.entity.User user = userMapper.selectOne(userQueryWrapper);
if(ObjectUtil.isEmpty(user)) {
throw new RuntimeException("用户不存在");
}
//todo 查询并设置对应的权限信息
List<String> data = menuMapper.selectPermsByUserId(user.getId());
return new LoginUser(user, data);
}
}
修改接口所需要的权限。
@RestController
public class baseController {
@RequestMapping("/hello")
//拥有text倾向才能访问
@PreAuthorize("hasAuthority('system:test:list')")
public String hello() {
return "hello!";
}
}
进行测试,最终成功。
1.自定义授权异常处理器和校验异常处理器。
//校验失败异常处理器
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//创建统一的返回对象,设置到response中
ResponseResult responseResult = new ResponseResult(HttpStatus.HTTP_UNAUTHORIZED, "校验失败!");
String json = JSON.toJSONString(responseResult);
//将统一的结果设置到Response中,本质就是将对应的数据设置到response中
WebUtil.renderString(response, json);
}
}
//授权失败异常处理器
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseResult responseResult = new ResponseResult(HttpStatus.HTTP_UNAUTHORIZED, "授权失败!");
String json = JSON.toJSONString(responseResult);
WebUtil.renderString(response, json);
}
}
对应的webUtil工具类
public class WebUtil {
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try
{
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
//将结果json以流的形式写入response中
response.getWriter().print(string);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}
2.将自定义的异常处理器进行配置
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
AccessDeniedHandler accessDeniedHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//将AuthenticationManager配置到ioc中
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//配置权限规则,主要就睡要放行登录接口,不然登录接口都会被了解,以及其他不要的前后端分离的配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//由于是前后端分离项目,所以要关闭csrf
.csrf().disable()
//由于是前后端分离项目,没有session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//指定让spring security放行登录接口的规则
.authorizeRequests()
// 对于登录接口 anonymous表示允许匿名访问, permitAll就是 登录和没登录都可以访问
.antMatchers("/user/login").anonymous() //匿名访问,未登录就可以访问
// 除上面外的所有请求全部需要鉴权认证后访问
.anyRequest().authenticated();
//将过滤器添加到用户登录处理器之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//设置自定义的异常处理器
http.exceptionHandling()
//校验异常处理器
.authenticationEntryPoint(authenticationEntryPoint)
//授权异常处理器
.accessDeniedHandler(accessDeniedHandler);
}
}
进行测试,异常显示成功。
1.开启springboot的允许跨域。
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
//重写spring提供的WebMvcConfigurer接口的addCorsMappings方法
public void addCorsMappings(CorsRegistry registry) {
//设置可以跨域的映射地址
registry.addMapping("/**")
// 设置可以跨域的源
.allowedOriginPatterns("*")
// 是否允许使用cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
2.开启springsecurity的允许跨域。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
AccessDeniedHandler accessDeniedHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//将AuthenticationManager配置到ioc中
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//配置权限规则,主要就睡要放行登录接口,不然登录接口都会被了解,以及其他不要的前后端分离的配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//由于是前后端分离项目,所以要关闭csrf
.csrf().disable()
//由于是前后端分离项目,没有session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//指定让spring security放行登录接口的规则
.authorizeRequests()
// 对于登录接口 anonymous表示允许匿名访问, permitAll就是 登录和没登录都可以访问
.antMatchers("/user/login").anonymous() //匿名访问,未登录就可以访问
// 除上面外的所有请求全部需要鉴权认证后访问
.anyRequest().authenticated();
//将过滤器添加到用户登录处理器之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//设置自定义的异常处理器
http.exceptionHandling()
//校验异常处理器
.authenticationEntryPoint(authenticationEntryPoint)
//授权异常处理器
.accessDeniedHandler(accessDeniedHandler);
//允许跨域
http.cors();
}
}
最终设置完成。
1.自定义校验类
@Component("itolen") //设置该类在ioc中的名称
public class ExpressionRoot {
//判断权限是否存在
public boolean hasAuthority(String authority) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
List<String> authorities2String = loginUser.getAuthorities2String();
return authorities2String.contains(authority);
}
}
2.在对应的接口上调用自定义方法。
@RestController
public class baseController {
@RequestMapping("/hello")
//拥有text倾向才能访问
@PreAuthorize("@itolen.hasAuthority('system:test:list')")
public String hello() {
return "hello!";
}
}
进行测试。
//认证成功处理器实现类
@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//认证成功后就会进行该方法
System.out.println("认证成功!");
}
}
//认证失败处理器实现类
@Component
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//认证失败后执行该方法
System.out.println("认证失败!");
}
}
?将两个类进行配置。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
//配置权限规则,主要就睡要放行登录接口,不然登录接口都会被了解,以及其他不要的前后端分离的配置
@Override
protected void configure(HttpSecurity http) throws Exception {
//设置校验成功处理器和校验失败处理器
http.formLogin().successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler);
}
}
@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//注销成功后执行的方法
System.out.println("注销成功!");
}
}
将该类进行配置。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
//配置权限规则,主要就睡要放行登录接口,不然登录接口都会被了解,以及其他不要的前后端分离的配置
@Override
protected void configure(HttpSecurity http) throws Exception {
//设置注销成功处理器
http.logout().logoutSuccessHandler(logoutSuccessHandler);
}
}