当项目中要用到用户的认证及权限的时候我们一般会使用 springSecurity来解决
引入
引入很简单
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>5.4.2</version>
</dependency>
先来看一下流程(认证流程)只是认证,不管权限
步骤是这样的:
- 用户提交用户名和密码, 由UsernamePasswordAuthenticationFilter 接收, 并由 AuthenticationManager 把它封装成一个 Authentication对象,注意, 此时 Authentication 对象中只用 用户提交的 用户名和密码
- 然后一直向后面传,一直到 UserDetailsService 的接口中, 从数据库中查到用户在数据库中的用户名,并通过用户名, 查到用户的所有信息, 并写入的 UserDetails 的类中
- 然后, 会通过 PasswordEncoder 来对比, Authentication中用户提交的密码,和 在数据库中查到的 UserDetails中的用户的 密码是否一至
4.最后,在securityContentHolder.getContext().setAuthentication(Authication),就把认证的对象写入到了上下文中
初级用法(只管认证,先不管权限)
从上面的步骤中, 我们可以看到, 认证方面比较重要的是 UserDetailsService 接口, 以及 PassEncoder 接口,一个是用来从数据库中拿到数据, 另一个是用来对比用户的输入是否正确 其中还会用到一个 UserDetails 的实体类的接口
当我们引入之后, 项目上的把的访问地址, 就会受到 springsecurity 的保护了, 当你请求任何接口时, 就会跳转到登录页面的, 用户名是 user , 密码是在控制台中打印出来的, 拿来用就可以了, 这当然不是我们要用的
我们说的初级用法是 从数据库中查到数据, 然后登录
实现
建一个用户表, 主要要有用户名和密码字段, 然后用 mybatisplus 生成出代码, 这个不是重点
接着, 我们要新建一个 UserDetails 的实例 SecurityUserDetails ,实现接口中的所有方法, 并添加一下后面要用的属性
package com.huang.security.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SecurityUserDetails implements UserDetails {
private SysUser sysUser; //这个是自已添加的属性, 主要是用来存放从数据库中查到的用户的信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
//return null;
//把这个方法的返回值, 改写成 sysUser 的password返回
return sysUser.getPassword();
}
@Override
public String getUsername() {
//return null;
//把这个方法的返回值,改成 sysUser 的 username的返回值
return sysUser.getUserName();
}
@Override
//注意, 这里是是否没有过期
public boolean isAccountNonExpired() { //把这里的是否 没有过期返回 true
return true;
}
@Override
//注意, 这里是是否没有锁 改成返回 true
public boolean isAccountNonLocked() {
return true;
}
@Override
//注意, 这里是是否认证没有过期 改成返回 true
public boolean isCredentialsNonExpired() {
return false;
}
@Override
//注意, 这里是是当前用户是否可以使用 改成返回 true
public boolean isEnabled() {
//return (Boolean)sysUser.getStatus();
return true;
}
}
实体类创建好之后, 我们就要在 UserDetailSerivce 接口中返回 新建的 SecurityUserDetails 实例
注意,这里只是通过用户我名称, 得到了用户的信息, 并且, 这个 loadUserByUsername() 的方法,是springSecurity 中帮我们调用的, 参数是自动传入的
package com.huang.security.service.impl;
import com.huang.security.mapper.SysUserMapper;
import com.huang.security.pojo.SecurityUserDetails;
import com.huang.security.pojo.SysUser;
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 java.util.Objects;
public class SecurityUserDetailServiceImpl implements UserDetailsService {
@Autowired //注入SysUserMapper 对象, 用来查找用户的信息
private SysUserMapper sysUserMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//根据用户名从数据库中查的找到用户的信息, 写入到 返回的 userDetails对象中
SysUser sysUser = sysUserMapper.selectOneByUserName(s);
if(Objects.isNull(sysUser)){
throw new RuntimeException("通过用户名没有查到用户的信息");
}
//这里就可以新建一个 UserDetails 对象, 把用户信息先放入了
SecurityUserDetails userDetails = new SecurityUserDetails();
userDetails.setSysUser(sysUser);
//todo 这里其实还可以从数据库中 获取用户的权限信息 写入 userDetails实例中的, 暂时先不考虑这些
//userDetails.getAuthorities(); 就是获取权限信息的字符串, 后面再说
return userDetails;
}
}
上面的是查找到了用户, 接着,就要让证了, 通过 PasswordEncode 接口的实现类来比对, 确定认证, SpringSecurity 有默认的 PasswordEncoder 来处理, 但是不太好用, 我们一般都是使用的 SpringSecurity提供的另外一个类 BCryptPasswordEncoder , 我们只需要注入一下就可以了
下图中, 要在方法上 添加一个 @Bean 注入
BCryptPasswordEncoder 中, springSecrity 为我们 写好了, 密码的生成方法, 以及 验证密码的方法。
所以, 从这里我们也可以构造我们自己的密码验证方法, 只需要写一个类去实再 PasswordEncoder接口, 并实现其中的两个方法就可以了, 然后注入到 spring容器中
比如 我们可以写一下 MD5PasswordEncoder implement PasswordEncoder 实现使用 Md5 加密和验证的方法,这里就暂且不说了
实现了以上步骤, 最基本的用户认证就算是完成了
前后端分离系统的认证方法
当我们做前后端分离的项目时, 我们一般不会在页面登录, 也就是说没有, springSecurity自带的登录方法。而是写一个登录接口
首先我们要配置一下 springSecurity ,不要使用 表单登录, 不要跳转到 loginpage 不使用 session 不使用 csrf 等
新建一个 springboot的配置类 securityConfig extend要 WebSecurityConfigurerAdapter
说明一下, 这个SecurityConfig 的配置类, 要继承 WebSecurityConfigurerAdapter , 同时还要加上 @EnableSecurity的注解
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//不使用 csrf
http.csrf().disable();
//不通过session获取 securityContext
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
//对于用户登录接口允许 匿名方问
.mvcMatchers("/user/login").anonymous()
//除上面的接口以外, 都需要认证才可以
.anyRequest().authenticated();
//http.formLogin() 这一个不要配置, 如果配置的话, 就会跳转到登录页
}
}
第一步, 通过登录页面 获取token
我们新建一个 UserController 其中写上 /user/login(因为上面配置了 /user/login可以匿名访问) 的方法, 这个方法返回一个 jwtToken 的字符串
SysUserService中的login方法
在这之前, 我们在 application.yml 中配置一个 jwt的key 的Base64的字符串, 后面好用来生成 jwt 的key
(这个Base64 的字符串又是怎么来的呢 ? 在 测试方法中生成了一个)
SysUserService中的login方法 代码如下
package com.huang.security.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.huang.security.mapper.SysRolesMapper;
import com.huang.security.mapper.SysRulesMapper;
import com.huang.security.pojo.SecurityUserDetails;
import com.huang.security.pojo.SysRoles;
import com.huang.security.pojo.SysRules;
import com.huang.security.pojo.SysUser;
import com.huang.security.service.SysUserService;
import com.huang.security.mapper.SysUserMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.impl.DefaultClaims;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.util.*;
/**
* @author Administrator
* @description 针对表【sys_user】的数据库操作Service实现
* @createDate 2023-06-05 09:44:59
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser>
implements SysUserService{
@Value("${myjwt.keyswithbase64}") //这里是从application.yml中得到了 jwtkey的字符串
private String jwtKeysBase64;
@Autowired(required = false)
private SysRolesMapper sysRolesMapper;
@Autowired(required = false)
private SysRulesMapper sysRulesMapper;
@Autowired(required = false) //这个是认证管理对象, 它是通过 SecurityConfig中的方法注入的, 后面说明它
private AuthenticationManager authenticationManager;
@Override
public String login(SysUser user) {
//从流程图中我们可以知道, Security 会使用 Authentication 对象来封装用户传递过来的 用户名和密码
//那么我们又是怎么调用认证的方法,并且使用我们 自已生成的 Authentication 对象来作为参数呢
//Security提供了 AuthenticationManager 类, 它可以在 Configuration中 重写方法, 得到 AuthenticationManager 对象, 并注入bean中
//然后,调用 authenticationManager的 authenticate(authentication), 在这个方法中传入 authentication 对象
//authenticate() 方法 内部, 就会自动去做认证, 调用 UserDetailService 中的方法, 从数据库中查找到用户, 并使用 PasswordEncoder 来认证用户
//如果认证成功, 返回的 该方法返回的对象,其中就可以得到 用户的信息
//如果认证不成功, 返回的结果是 null
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
//这里new的时候只是在里面 放入了 username 和 password 两个参数, 当认证通过了之后, Security会把认证过后的信息, 也就是 SecurityDetails 的对象写入 authenticate 对象中, 使用 authenticate.getPrincipal()
Authentication authenticate = authenticationManager.authenticate(authentication);
if(Objects.isNull(authenticate)){
throw new RuntimeException("用户认证失败");
}
//如果不为空, 说明用户的认证通过
//此时就要用 jwt 为用户生token
SecurityUserDetails userDetails = (SecurityUserDetails)authenticate.getPrincipal();
SysUser sysUser = userDetails.getSysUser();
//从认证的信息中获取用户的信息
SecretKey secretKey = Keys.hmacShaKeyFor(Base64.getDecoder().decode(jwtKeysBase64));
Claims claims = new DefaultClaims();
claims.put("userId", sysUser.getId());
// 把用户的id 和过期时间设置一下, 生成token
String jwttoken = Jwts.builder().setClaims(claims).setExpiration(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000)).signWith(secretKey).compact();
return jwttoken;
}
AuthenticationManager 类是怎么得到的, 在哪里注入的?
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtFilter jwtFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
//不使用 csrf
http.csrf().disable();
//不通过session获取 securityContext
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
//对于用户登录接口允许 匿名方问
.mvcMatchers("/user/login").anonymous()
//除上面的接口以外, 都需要认证才可以
.anyRequest().authenticated();
//http.formLogin() 这一个不要配置, 如果配置的话, 就会跳转到登录页
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean //注意这里!!!!!!!!!!!!!!!!!!!!!!!!!!!
//当我们自定义认证的时候要使用 AuthenticationManager.authention方法来完成认证
//重写这个方法再加入@Bean就可以注入使用了
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
到这里, 我们请求 /user/login方法的时候,就得到了一个 jwttoken, 前端存下来之后, 在后面的请求中就可以把这个 token 放在请求头中, 进行请求了
第二步, 带token 去请求其它的接口, 并且在接口中可以得到当前用户的信息
当前端请求其它接口时, header 头中, 带上 token
如:我们在 userController 中请建了一个 接口
//UserController path: /user/visit
@PostMapping("/visit")
public String visit(HttpServletRequest request){
//为什么可以 getAttribute("user") , 因为我们在 过滤器中认证完用户之后, 把它写入到了 reqeust 的属性中
SysUser sysUser = (SysUser) request.getAttribute("user");
System.out.println(sysUser);
//我们同样也可以使用 SecurityContextHolder.getContext().getAuthentication() 得到的是 自定义的
//SecurityUserDetails 的对象, 它内部也有 Sysuser 的信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SecurityUserDetails userdetail = (SecurityUserDetails) authentication.getPrincipal();
SysUser sysUser2 = userdetail.getSysUser();
System.out.print("sysuer2"+sysUser2);
return "haha";
}
实现步骤
我们要创建一个 Filter , 来过滤 header 中的 token 并进生认证 过滤器代码如下
package com.huang.security.filter;
import com.huang.security.pojo.SecurityUserDetails;
import com.huang.security.pojo.SysUser;
import com.huang.security.service.SysUserService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.crypto.SecretKey;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Base64;
//这里没有使用 @WebFilter (也就是说没有 会使用 implement Filter) 是因为 springboot 的版本太多, 有的版本的 Filter 会无故的执行两次或多次。
//所以我们这里使用了 springboot 提供的一个 OncePerRequestFilter 类, 它可以保证 只执行一次, 当然要注意把它 加入到容器中 使用 @Component注解
//用法和 Filter 的用法是一样的
@Component
public class JwtFilter extends OncePerRequestFilter {
@Value("${myjwt.keyswithbase64}")
private String jwtKeysBase64;
@Autowired
private SysUserService sysUserService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//我们可以从 request 的头中获取token , 但是我们要注意这里, 如果header中没有token , 要让它放行, 不能抛了错误
//因为我们确实有的请求头是不需要token的, 但是也不用担心, 在SpringSecurity中 还会有后面的过滤器, 对这个请求进行身份的验证,
String token = httpServletRequest.getHeader("token");
if(!StringUtils.hasText(token)){
filterChain.doFilter(httpServletRequest,httpServletResponse);
//这里必须要使用 return , 如果没有使用 return , 根据洋葱皮的模型, 这个请求经过后面的过滤器之后, 还会回到当前的这个位置继续执
//行后面的代码, 所在要在这里 return 一下
return;
}
//如果token不为空, 我们就可以解析token, 然后得到用户的信息, 放入 redis中,或者 threadLocal中, 或者放到 request中了
//解析token
//获取一个key
SecretKey secretKey = Keys.hmacShaKeyFor(Base64.getDecoder().decode(jwtKeysBase64));
Claims claims = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody();
//这里的 claims 对象, 就是我们在 生成token时写入的 userId 的内容
//这里可以使用
SysUser user = sysUserService.getById(claims.get("userId").toString());
//获取到了user对象 我们要在 SecurityContext中写入, 因为后面的 Security的过滤器,是会使用 SecurityContextHolder来获取认证信息的
// 调用 SecurityContextHolder.getContext().setAuthentication()时, 发现要传入一个 Authentication 对象,
// Authentication 是一个接口, 所以我们使用了他的子类 UsernamePasswordAuthenticationToken
// 同时要注意, 我们在 new UsernamePasswordAuthenticationToken()的时候, 要使用 三个参数的构造函数
/*Object principal 就是我们自定义的 SecurityUserDetails 的对象
* public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); //这里为认证信息设置了状态, 后面的Security 的过滤器都会使用
}
*
* */
SecurityUserDetails userDetails = new SecurityUserDetails();
userDetails.setSysUser(user);
//todo 这里的第三个参数 其实是一个 权限的数组, 这里还没有做权限设置, 所在先传 null
UsernamePasswordAuthenticationToken authention = new UsernamePasswordAuthenticationToken(userDetails, null, null);
SecurityContextHolder.getContext().setAuthentication(authention);
//上面的工作完成之后, 就可以放行了
httpServletRequest.setAttribute("user",user);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
上面, 我们新建了一个过滤器, 并在过滤器中, 对用户进行了 token 的认证, 那么我们知道, 一个项目中都是有过滤器链的, 我们自定义的过滤器, 在SecurityContextHolder中写入认证结果, 所以, 我们自定义的过滤器应该放在 Secruity 的认证过滤器之前才对, 怎么才可以放在之前, 放在哪个之前呢
作法, 我们可以在SecurityConfig的配置文件中 进行设置
有了上面的操作, 把第一步中的 jwtToken 拿来使用, 请求 /user/visit 接口
可以看到, 测试是成功的
带用户权限认证的用法
我们从数据库中做 RBAC 的权限认证表, 一共五个表, 下面只捡要的字段创建一下这五个表
用户表 角色表 权限表 用户角色表 角色权限表
权限表字段
角色表字段
角色权限表字段
用户角色表
用户表字段主要是用户名和password 这里略过表中数据如下, 用户表
两个用户 huangjunhui 的角色是 主管 ceozhang 的角色是 ceo
这个是角色表
这个是用户角色表
这个是权限表
角色权限表 可以看到 ceo 有权限1和2 部门管理和测试都可以, 角色主管,就只有测试的功能了
以上就是 RBAC 的准备工作了开始 带权限的认证
首先, 我们要修改 Security 的配置类
之后我们要在 我们自定义的 userDetails 的类中, 也就是 SecruityUserDetails 的数中, 添加 根限相应的属性
然后, 我们就要在 自定义的 jwtFilter中, 创建UsernamePasswordAuthenticationToken 时候, 把用户的时候其中有三个参数, 把用户的权限信息写入
在Controller 中注解 @PreAuthorize(“hasAuthority(‘system:dept:list’)”)