SpringSecurity笔记

目录

摘要:此博客内容来源于bibi三更草堂的视频三更草堂的个人空间_哔哩哔哩_bilibili

1.准备工作

1.1创建项目

2.源码初探

3.修改代码让登陆走我们自己的接口

3.1创建一个maven的新模块

3.2创建数据库导入mysql和mybatisplus依赖

3.3总结一下登陆流程

 4.用户密码加密和JWT的基本使用

4.1加密存储密码

 4.2JWT的使用

5.自定义登录接口

5.1编写自己的控制层和业务层

5.2流程讲解

5.2.1放行我们登录的接口

5.2.2具体实现

6.Jwt过滤器

6.1获取前端请求头中的Token进行解析

6.2从redis中获取用户信息

6.3获取权限信息封装到authentication中

6.4配置我们的过滤器在UsernamePasswordAuthenticationFilter前面执行在Securty的配置文件中的configure方法

6.5完整代码

7.退出登录

8.授权实现

 8.1限制访问资源所需权限

 8.2封装权限信息



摘要:此博客内容来源于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语句如下

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值