目录
!!!我们主要改的就是 userDetailService实现类的里面的逻辑,把它改成到数据库查询
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个
SpringSecurity
的默认登陆页面,默认
用户名是user,密码会输出在控制台。
必须登陆之后才能对接口进行访问。
登录验证流程
原理分析
想要知道如何实现自己的登陆流程就必须要先知道入门案例中 SpringSecurity 的流程。SpringSecurity完整流程
SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。 (每次请求都会经过这个链路,密码校验等等,但是 我们只有在第一次登陆的时候输密码,其他的时候不输入密码呀,那以后的请求怎么经过这个链路呢,我们可以在第一次登录的时候,登陆成功后保存生成一个jwt用来保存用户id,其他的认证通过的用户信息保存到redis中,以后每次请求都解析jwt,从redis中取出用户认证信息然后保存到存入 SecurityContextHolder(然后其他的过滤器就可以从里面获取用户信息进行验证)) UsernamePasswordAuthenticationFilter : 负责处理我们在登陆页面填写了用户名密码后的登陆请 求。入门案例的认证工作主要有它负责。 ExceptionTranslationFilter : 处理过滤器链中抛出的任何 AccessDeniedException 和 AuthenticationException 。 FilterSecurityInterceptor : 负责权限校验的过滤器。通俗一点就是授权由它负责。(鉴权)!!!我们主要改的就是 userDetailService实现类的里面的逻辑,把它改成到数据库查询
前后端分离的实现思路
思路流程
登录
①自定义登录接口 调用 ProviderManager 的方法进行认证如果认证通过生成jwt(jwt只是用来存 用户id ) 把用户信息存入 redis 中 ②自定义 UserDetailsService 在这个实现类中去查询数据库 校验: ①定义 Jwt 认证过滤器 获取 token 解析 token 获取其中的 userid 从 redis 中获取用户信息 存入SecurityContextHolder(然后其他的过滤器就可以从里面获取用户信息进行验证)第一步:实现userdetailservice
因为最终调用这个方法完成认证, loadUserByUsername是providerManager进行调用的(不会显式的进行调用),传递过来的值只有一个用户名,这这个方法里面去数据库查找对象
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户信息
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(wrapper);
//如果查询不到数据就通过抛出异常来给出提示
if(Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
// 查询权限信息
List<String> perms = menuMapper.selectPermsByUserID(user.getId());
//封装成UserDetails对象返回,所以定义一个了LoginUser类实现UserDetails接口
System.out.println(new LoginUser(user,perms));
return new LoginUser(user,perms);
}
}
实现 UserDetails接口时,把下面哪些是否可用,没有超时等的哪些返回值全改为true
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
private List<String> permissions;
@JSONField(serialize = false) //不能序列化SimpleGrantedAuthority包
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;
//把权限信息封装
//1.传统写法
List<GrantedAuthority> res = new ArrayList<>();
for (String permission : permissions) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
res.add(simpleGrantedAuthority);
}
//2.stream流写法
// 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;
}
配置security实现密码加密存储
实际项目中我们不会把密码明文存储在数据库中。 默认使用的 PasswordEncoder 要求数据库中的密码格式为: {id}password 。它会根据 id 去判断密码的 加密方式。但是我们一般不会采用这种方式。所以就需要替换 PasswordEncoder 。 我们一般使用 SpringSecurity 为我们提供的 BCryptPasswordEncoder 。 我们只需要使用把 BCryptPasswordEncoder 对象注入 Spring 容器中, SpringSecurity 就会使用该 PasswordEncoder 来进行密码校验。 我们可以定义一个 SpringSecurity 的配置类, SpringSecurity 要求这个配置类要继承 WebSecurityConfigurerAdapter 。springboot 3版本中这个继承的这个适配器已经弃用,使用的是
/ @EnableWebSecurity 用EnableWebSecurity注解代替继承
登录接口
整体思路 :接下我们需要自定义登陆接口,然后让 SpringSecurity 对这个接口放行 , 让用户访问这个接口的时候不用登录也能访问。 在接口中我们通过AuthenticationManager 的 authenticate 方法来进行用户认证 , 所以需要在 SecurityConfig中配置把 AuthenticationManager 注入容器。 认证成功的话要生成一个 jwt ,放入响应中返回。并且为了让用户下回请求时能通过 jwt 识别出具体的是哪个用户,我们需要把用户信息存入redis ,可以把用户 id 作为 key 。
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf,因为前后端分离项目不需要csrf,所以直接关闭
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 把jwtAuthenticationTokenFilter 添加到过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Service
public class LoginServcieImpl implements LoginServcie {
@Autowired // 这个是在配置中配置的,见上图
AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(User user) {
//这个对象需要传递两个参数,一个是用户名,一个密码
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
// 第一步,authenticationManager调用authenticate方法,
//该方法需要传递 Authentication类型的参数,我们找他的实现类就好,
//该方法会去调用UserDetailsServiceImpl.loadUserByUsername
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 如果认证没通过,authenticate就是null
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败");
}
// 如果认证通过,使用userid生成jwt
//使用userid生成token
//getPrincipal()就是返回的我们userdetailservice实现类返回的对象
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
//authenticate存入redis
redisCache.setCacheObject("login:"+userId,loginUser);
//把token响应给前端
HashMap<String,String> map = new HashMap<>();
map.put("token",jwt);
return new ResponseResult(200,"登陆成功",map);
}
}
至此 登录部分实现完毕
校验
认证过滤器 我们需要自定义一个过滤器,这个过滤器会去获取请求头中的 token ,对 token 进行解析取出其中的 userid 。 使用 userid 去 redis 中获取对应的 LoginUser 对象。 然后封装 Authentication 对象存入 SecurityContextHolder@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token,因为如果第一次登录肯定没有token,所以直接放行,让后面的过滤器处理它
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//没有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 有两种构造方法,我们需要选择三种参数的
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
UsernamePasswordAuthenticationToken 两种构造方法,这里选择三个参数的,第一个参数传递用户id,第二个为null(因为是登录过了所以不用传递密码),第三个权限
看里面的内容主要是
setAuthenticated 这个值的不同,因为是从redis中取出来认证信息了,所以肯定是认证状态,所以
setAuthenticated是true,后面的过滤器发现是已认证状态就会通过
这个jwt过滤器写完不能直接放到spring容器中需要进行配置,让他经过过滤器链,那么放在哪呢,肯定是放在最前面啊,放在后面那就没意义了
这个方法的意思就是放在哪个过滤器前面
退出登录
我们只需要定义一个登陆接口,然后获取 SecurityContextHolder 中的认证信息,删除 redis 中对应的数据即可 @Override
public ResponseResult logout() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userid = loginUser.getUser().getId();
redisCache.deleteObject("login:"+userid);
return new ResponseResult(200,"退出成功");
}
授权
例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。 总结起来就是 不同的用户可以使用不同的功能 。这就是权限系统要去实现的效果。 我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。 所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。 在 SpringSecurity 中,会使用默认的 FilterSecurityInterceptor 来进行权限校验。在 FilterSecurityInterceptor 中会从 SecurityContextHolder 获取其中的 Authentication ,然后获取其中的 权限信息。当前用户是否拥有访问当前资源所需的权限。 所以我们在项目中只需要把当前登录用户的权限信息也存入 Authentication 。 然后设置我们的资源所需要的权限即可。 在写 UserDetailsServiceImpl 的时候说过,在查询出用户后还要获取对应的权限信息,封装到 UserDetails 中返回。
RBAC权限模型
RBAC 权限模型( Role-Based Access Control )即:基于角色的权限控制。这是目前最常被开发者使用 也是相对易用、通用权限模型。自定义验证异常类
创建 exception 包,在 exception 包下创建自定义 CustomerAuthenticationException 类,继承 AuthenticationException 类认证失败的
/**
* 认证失败处理类 未认证返回的异常,在访问未经授权的时候触发
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse
response, AuthenticationException authException) throws IOException, ServletException {
ResponseResult result = new
ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录111");
String json = JSON.toJSONString(result);
WebUtils.renderString(response, json);
}
}
授权失败的
跨域
浏览器出于安全的考虑,使用 XMLHttpRequest 对象发起 HTTP 请求时必须遵守同源策略,否则就是跨域的HTTP 请求,默认情况下是被禁止的。同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。 前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。 所以我们就要处理一下,让前端能进行跨域请求。 ①先对 SpringBoot 配置,运行跨域请求// springboot的跨域配置
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
security配置文件中配置 security的跨域
自定义权限校验方法
我们模仿的是官方的注解,写自定义的注解,官方注解返回的是true false 所以 我们自定义的也返回true和false就行