目录
摘要:此博客内容来源于bibi三更草堂的视频三更草堂的个人空间_哔哩哔哩_bilibili
6.4配置我们的过滤器在UsernamePasswordAuthenticationFilter前面执行在Securty的配置文件中的configure方法
摘要:此博客内容来源于bibi三更草堂的视频三更草堂的个人空间_哔哩哔哩_bilibili
1.准备工作
1.1创建项目
先创建一个maven项目然后导入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--SpringbootWeb依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<!--SpringSecurity依赖-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
1.2启动项目
只要引入了安全框架就会有这个
2.源码初探
这个是springsecurity的基本流程
3.修改代码让登陆走我们自己的接口
3.1创建一个maven的新模块
加依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<!--SpringSecurity依赖-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<!--json依赖-->
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<dependency>
<!--jwt依赖-->
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<!--redis依赖-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
这里我们可能会用redis进行登陆流程
3.2创建数据库导入mysql和mybatisplus依赖
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/springboot-security
password: root
username: root
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<version>2.5.6</version>
<scope>test</scope>
</dependency>
这个是数据库表
CREATE TABLE `sys_user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
`password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` CHAR(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
`email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱',
`phonenumber` VARCHAR(32) DEFAULT NULL COMMENT '手机号',
`sex` CHAR(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` VARCHAR(128) DEFAULT NULL COMMENT '头像',
`user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
`create_by` BIGINT(20) DEFAULT NULL COMMENT '创建人的用户id',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`del_flag` INT(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'
然后配置一下mybatisplus的mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fen.domain.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user")
public class User implements Serializable {
private static final long serialVersionUID = -40356785423868312L;
/**
* 主键
*/
@TableId
private Long id;
/**
* 用户名
*/
private String userName;
/**
* 昵称
*/
private String nickName;
/**
* 密码
*/
private String password;
/**
* 账号状态(0正常 1停用)
*/
private String status;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phonenumber;
/**
* 用户性别(0男,1女,2未知)
*/
private String sex;
/**
* 头像
*/
private String avatar;
/**
* 用户类型(0管理员,1普通用户)
*/
private String userType;
/**
* 创建人的用户id
*/
private Long createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新时间
*/
private Date updateTime;
/**
* 删除标志(0代表未删除,1代表已删除)
*/
private Integer delFlag;
}
接下来我们需要写一个UserDetailsService实现类定义自己的登陆逻辑不走安全框架的默认
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fen.dao.UserMapper;
import com.fen.domain.LoginUser;
import com.fen.domain.User;
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.Objects;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
UserMapper mapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
LambdaQueryWrapper<User>queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = mapper.selectOne(queryWrapper);
if (Objects.isNull(user)){
throw new RuntimeException("用户名错误");
//用户为空抛出异常没问题的话就让他往下走
}
// 查询用户权限信息
return new LoginUser(user);
}
}
因为这个方法返回的是UserDetails类型我们写一个类继承这个接口
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.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
public LoginUser(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
System.out.println(user.getPassword());
return user.getPassword();
// 密码
}
@Override
public String getUsername() {
System.out.println(user.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;
}
}
我们先到数据库插入一条数据
然后启动springboot
输入我们的账号密码
我们就登陆进来了
3.3总结一下登陆流程
我们自己实现UserDetailsService接口把原来的替换成自己的数据库
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
UserMapper mapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
LambdaQueryWrapper<User>queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = mapper.selectOne(queryWrapper);
if (Objects.isNull(user)){
throw new RuntimeException("用户名错误");
//用户为空抛出异常
}
// 查询用户权限信息
return new LoginUser(user);
//这里返回值类型是UserDetails我们要写一个这个的实现类
}
}
这个返回值类型是UserDateils对象那我们还要实现一下这个接口
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
public LoginUser(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
// 获取权限信息
}
@Override
public String getPassword() {
System.out.println(user.getPassword());
return user.getPassword();
// 密码
}
@Override
public String getUsername() {
System.out.println(user.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;
}
}
这里用户名和密码都是数据库里面的注意的是密码如果要明文存储的话就要加一个{noop}
4.用户密码加密和JWT的基本使用
4.1加密存储密码
创建一个Security的配置类官方建议配置类都要继承这个WebSecurityConfigurerAdapter
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder PasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
接下来我们测试一下这个这么用
@Test
public void PasswordEncoder(){
PasswordEncoder pass = new BCryptPasswordEncoder();
String encode = pass.encode("12345");
String encode2 = pass.encode("12345");
System.out.println(encode);
System.out.println(encode2);
}
明文都一样的加密后却不一样
在看一下解密的方法
@Test
public void Test2(){
PasswordEncoder pass = new BCryptPasswordEncoder();
List<User> users = mapper.selectList(null);
users.forEach(user -> {
//解密的方法
boolean matches = pass.matches( "1234567890",user.getPassword());
if (matches){
System.out.println("没问题");
}else {
System.out.println("解密有问题");
}
});
}
4.2JWT的使用
public static void main(String[] args) throws Exception {
String jwt = createJWT("2123"); //加密
Claims claims = parseJWT(jwt);
String subject = claims.getSubject(); //解密
System.out.println(subject);
// System.out.println(claims);
}
5.自定义登录接口
5.1编写自己的控制层和业务层
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@RequestMapping("/user/login")
public ResponseResult login(@RequestBody User user){
System.out.println(user);
return loginService.login(user);
}
}
@Service
public interface LoginService {
ResponseResult login(User user);
}
@Service
public class LoginServicelmpl implements LoginService {
@Autowired
private AuthenticationManager manager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(User user) {
// AuthenticationManager manager 进行认证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
Authentication authenticate = manager.authenticate(authenticationToken);
// 认证失败我们要给出提示
if (Objects.isNull(authenticate)){
return new ResponseResult(500,"登陆失败",null);
}
// 如果认证通过了我们用userid生成一个jwt返回给前端
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
HashMap<String,String> map = new HashMap<>();
map.put("token",jwt);
// 把完整的信息存入redis user作为key
redisCache.setCacheObject("login:"+userId,loginUser);
return new ResponseResult(200,"登录成功",map);
}
}
5.2流程讲解
5.2.1放行我们登录的接口
方行我们接口后,我们使用springSecurity的AuthenticationManager来验证账号密码是否正确
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder PasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf
http
.csrf().disable()
// 不通过session获取securityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对这个接口可以匿名访问
.antMatchers("/user/login").anonymous()
// 除了上面外的所有请求都需要愣怔
.anyRequest().authenticated();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
5.2.2具体实现
我们要用到AuthenticationManager来验证,把他注入到容器这个类会自动调用UserDetailsService
我们上面用一个类实现了UserDetailsService他会走我们数据库查询
@Autowired
private AuthenticationManager manager;
因为他获取认证需要一个Authentication这个类型所以我们找一下他的实现类UsernamePasswordAuthenticationToken,我们new一个这个对象把账号密码传进去在把这个对象注入到AuthenticationManager中去
// AuthenticationManager manager 进行认证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
Authentication authenticate = manager.authenticate(authenticationToken);
// 认证失败我们要给出提示
if (Objects.isNull(authenticate)){
return new ResponseResult(500,"登陆失败",null);
}
成功之后我们用AuthenticationManager这个对象获取userid来生成jwt并把对应信息存入redis
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
HashMap<String,String> map = new HashMap<>();
map.put("token",jwt);
// 把完整的信息存入redis user作为key
redisCache.setCacheObject("login:"+userId,loginUser);
完整代码
@Service
public class LoginServicelmpl implements LoginService {
@Autowired
private AuthenticationManager manager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(User user) {
// AuthenticationManager manager 进行认证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
Authentication authenticate = manager.authenticate(authenticationToken);
// 认证失败我们要给出提示
if (Objects.isNull(authenticate)){
return new ResponseResult(500,"登陆失败",null);
}
// 如果认证通过了我们用userid生成一个jwt返回给前端
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
HashMap<String,String> map = new HashMap<>();
map.put("token",jwt);
// 把完整的信息存入redis user作为key
redisCache.setCacheObject("login:"+userId,loginUser);
return new ResponseResult(200,"登录成功",map);
}
}
6.Jwt过滤器
创建我们的Jwt过滤器JwtAuthenticationTokenFilter继承OncePerRequestFilter接口实现doFilterInternal方法
6.1获取前端请求头中的Token进行解析
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
filterChain.doFilter(request, response);
// 这里为什么要放行呢因为他没有token我们后面是解析token
// 他没有token会被后面的拦截器拦截
return;
}
// 解析Token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Token非法");
}
6.2从redis中获取用户信息
String key = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(key);
if (Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
6.3获取权限信息封装到authentication中
因为我们没有权限信息我们两个参数填null
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(request,response);
6.4配置我们的过滤器在UsernamePasswordAuthenticationFilter前面执行在Securty的配置文件中的configure方法
// 把我们定义的JWT过滤器放到这个账号密码过滤器前面
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
6.5完整代码
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取传过来的Token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
filterChain.doFilter(request, response);
// 这里为什么要放行呢因为他没有token我们后面是解析token
// 他没有token会被后面的拦截器拦截
return;
}
// 解析Token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Token非法");
}
// 从redis中获取用户信息
String key = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(key);
if (Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
// 存入SecurityContextHolder
// TODO 获取权限信息封装到authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(request,response);
}
}
7.退出登录
退出登录其实很简单创建controller和service层我们具体看service的实现
@Override
public ResponseResult logout() {
// 获取SecurityContextHolder中的用户id
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long id = loginUser.getUser().getId();
// 删除redis中的用户信息
redisCache.deleteObject("login:"+id);
return new ResponseResult(200,"注销成功");
}
代码的注释很清楚
8.授权实现
8.1限制访问资源所需权限
springSecurity可以使用注解和配置的方式来配置对应接口的权限
首先在配置了上加上这个注解来开启Security的注解支持
@EnableGlobalMethodSecurity(prePostEnabled = false)// 开启注解功能
下面这个是方法上的一个注解,这个注解的含义是必须要有test权限才能访问这个接口
@RequestMapping("/hello")
@PreAuthorize("hasAnyAuthority('test')")
public String Hello(){
return "hello";
}
8.2封装权限信息
首先在LoginUser里面添加一个参数
private List<String> permissions;
public LoginUser(User user,List<String> permissions) {
this.user = user;
this.permissions=permissions;
}
添加完了之后在添加他的一个构造函数然后把权限信息添加到userDatails的权限方法中
上面这个JSONFieLd的注解是不把他反序列化存储到redis中去
@JSONField(serialize = false)
private List<GrantedAuthority>authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//把permissions的string类型封装成SimpleGrantedAuthority对象
if (authorities!=null){
return authorities;
}
// List<SimpleGrantedAuthority>authorities =permissions.stream()
// .map(SimpleGrantedAuthority::new)
// .collect(Collectors.toList());
List<GrantedAuthority>authorities = new ArrayList<>();
for (String s:permissions) {
authorities.add(new SimpleGrantedAuthority(s));
}
// 获取权限信息
return authorities;
}
9.RBAC权限模型
9.1表设计
这个就是简单的表设计SQL语句如下