Spring Security学习笔记

Spring Security学习笔记


1、Spring Security简介:

​ Spring Security是基于Spring的一套权限框架,它有两大重要核心功能:用户认证和用户授权。用户认证指的是某个用户能否访问该系统,用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般在系统中,不同的用户会分配不同的角色,不同的角色也对应不同的权限。

2、Spring Security历史

​ Spring Security 诞生于 2003 年年底,最先叫做“Spring的acegi安全系统”,在2006年的时候发布了1.0.0的版本,在2007年年底的时候正式成为Spring全家桶的一个成员,并更名为“Spring Security”。

3、Spring Security与Shiro对比

Spring Security: 是Spring全家桶的一个部分,它功能强大,能很好的与Spring进行整合,但它是重量级的一个框架,和ssm相整合需要进行很多的繁琐配置(后期可详细研究进行哪些配置),Spring Boot 对于 Spring Security 提供了自动化的配置方案,可以使用更少的配置来使用Spring Security。

Shiro: 是Apache旗下的一款轻量级的权限框架,相对于Spring Security来说功能更少,和ssm相整合也没有Spring Security那么复杂。

4、创建Spring Boot/Spring Security小demo

1、导包:

<!-- spring security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.6.7</version>
</dependency>

2、controller代码:

@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping("/hello")
    public String hello(){
        return "hello security!";
    }
}

3、浏览器访问:localhost:8080/test/hello,需登录访问。Spring Security默认的用户名为user,密码在idea控制台,如下图所示:

请添加图片描述
请添加图片描述
请添加图片描述

5、Spring Security基本原理

​ Spring Security本质上是一个过滤器链,它的底层采用的是责任链的设计模式,它有一条很长的过滤器链,在启动类中随便写一行打印代码,然后打断点调试。点击下面计算器的图标,在输入框中输入:run.getBean(DefaultSecurityFilterChain.class)按回车就可查看15条过滤器链。

请添加图片描述

每一个过滤器功能具体见链接:

https://blog.csdn.net/K_520_W/article/details/118855281
6、Spring Security两个重要接口(UserDetailsService和passwordEncoder)

1、UserDetailsService:用于查询数据库和密码,在SpringSecurity中,如果不连接数据库则自动分配一个user用户,密码随机生成,如果要连接数据库,则连接数据库并编写UserDetailsService实现类,数据库中的用户即为认证用户。

2、passwordEncoder:是Spring Security中的一个密码解析接口,其中有三个方法,如下图所示:
在这里插入图片描述

encode(): 是对字符串进行加密的方法。

matches():校验传入的明文密码rawPassword和加密密码encodedPassword是否相匹配。

upgradeEncoding() :此方法目前我还未用到过。

7、Spring Security 的web权限方案(3种方案)

方案一:通过在application.yml中配置用户名和密码实现登录的用户。

Spring:
  security:
    user:
      name: zhangsan
      password: 123

方案二:通过配置类配置。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder().encode("123");
        //配置账户:lisi 密码:123
        auth.inMemoryAuthentication().withUser("lisi").password(password).roles("admin");
    }
    
    @Bean
    PasswordEncoder password(){
        return new BCryptPasswordEncoder();
    }
}

方案三:通过配置类和实现类返回user对象,user对象有用户名密码和操作权限。(实际开发中用的最多的就是第三种)

1)Security配置类:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(password());
    }

    @Bean
    PasswordEncoder password(){
        return new BCryptPasswordEncoder();
    }
}

2)UserDetailsService的实现类:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        return new User("wangwu", new BCryptPasswordEncoder().encode("123"), auths);
    }
}
8、通过与数据库交互实现权限用户

此处演示使用的是MyBatis-plus:

1)先导入MyBatis-plus、Mysql8.x、lombok依赖:

<!-- mybatis-plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

2)创建数据库、实体类、mapper接口service接口及impl实现类:

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=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';

插入数据:名为密码为123

INSERT INTO `sys_user` VALUES (1, 'fll', '佚名', '$2a$10$vhhJr0g0Wxtf4QYQ/NzEVOTspp9SJG2PmH9rbaxBAgFgxepgcGiRW', '0', '', NULL, NULL, NULL, '1', NULL, NULL, NULL, NULL, 0);
INSERT INTO `sys_user` VALUES (2, 'test', '测试', '$2a$10$vhhJr0g0Wxtf4QYQ/NzEVOTspp9SJG2PmH9rbaxBAgFgxepgcGiRW', '0', NULL, NULL, NULL, NULL, '1', NULL, NULL, NULL, NULL, 0);

SysUser实体类:

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "sys_user")
public class SysUser 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;
}

UserDetails接口:

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private SysUser user;

    //存储权限信息
    private List<String> permissions;

    public LoginUser(SysUser user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    //存储SpringSecurity所需要的权限信息的集合
    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities != null) {
            return authorities;
        }
        //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
        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;
    }
}

SysUserMapper接口:

@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
}

SysUserService接口:

public interface SysUserService extends IService<SysUser> {
    ResultOK login(SysUser user);
}

SysUserServiceImpl实现类:

@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public ResultOK login(SysUser user) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("用户名或密码错误");
        }

        //使用userid生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);

        //authenticate存入redis
        redisUtil.setCacheObject("login:" + userId, loginUser);

        //把token响应给前端
        HashMap<String, String> map = new HashMap<>(1);
        map.put("token", jwt);
        return new ResultOK(200, "登陆成功!", map);
    }
}

UserDetailsServiceImpl实现类:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private SysMenuMapper sysMenuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名查询用户信息
        QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_name", username);
        SysUser user = sysUserMapper.selectOne(queryWrapper);

        //如果查询不到数据就通过抛出异常来给出提示
        if (Objects.isNull(user)) {
            throw new RuntimeException("用户名或密码错误!");
        }

        //TODO 根据用户查询权限信息 添加到LoginUser中
        //从数据库中查
        List<String> list = sysMenuMapper.selectPermsByUserId(user.getId());
        //封装成UserDetails对象返回
        return new LoginUser(user, list);
    }
}

3)编写UserController:

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private SysUserService sysUserService;

    @PostMapping("/login")
    public ResultOK login(@RequestBody SysUser sysUser){
        return sysUserService.login(sysUser);
    }

}

4)AccessDeniedHandler实现类(Security自定义失败处理-没有权限)

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResultOK resultOK = new ResultOK(403, "权限不足!");
        String json = JSON.toJSONString(resultOK);
        response.setStatus(403);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(json);
    }
}

5)AuthenticationEntryPoint实现类(Security自定义处理-未登录或者token过期)

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        //ResultOK resultOK = new ResultOK(HttpStatus.FORBIDDEN.value(), "您尚未登录,请登录后操作!");
        //若不知道填啥,可在HttpStatus进行枚举检查
        ResultOK resultOK = new ResultOK(401, "用户认证失败,请查询登录!");
        String json = JSON.toJSONString(resultOK);
        response.setStatus(401);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(json);
    }
}

6)OncePerRequestFilter类(Security的 jwt 过滤器)

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        //如果字符串里面的值为null, "", "  ",那么返回值为false;否则为true
        if (!StringUtils.hasText(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 = redisUtil.getCacheObject(redisKey);

        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        //存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }

}

7)使用postman进行测试:

http://localhost:8080/user/login

请添加图片描述

测试成功!

9、Spring Security中hasRole()、hasAnyRole()、hasAuthority()和hasAnyAuthority()四个方法的区别

1)hasRole(role):用户拥有指定的角色权限时返回true

2)hasAnyRole([role1,role2]):用户拥有任意一个指定的角色权限时返回true

3)hasAuthority(authority):用户拥有指定的权限时返回true

4)hasAnyAuthority([authority1,authority2]):用户拥有任意一个指定的权限时返回true

这四个方法均用在controller方法前面,配合@PreAuthorize使用:

例:

@GetMapping("/hello")
//进入方法前进行权限验证
@PreAuthorize("hasAuthority('system:dept:index')")
public String hello(){
    return "部门管理perims";
}
10、通过Security Config配置类配置权限
@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //白名单
                //anonymous() 允许匿名用户访问,不允许已登入用户访问
                //permitAll() 不管登入,不登入 都能访问
                //.antMatchers("/test/hello").permitAll()
                .antMatchers("/user/login").anonymous()
                //配置权限访问
                .antMatchers("/test/hello").hasAuthority("system:dept:index")
                //任意的用户认证过后都能访问
                .anyRequest().authenticated();
        //添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //配置异常处理
        http.exceptionHandling()
                //配置认证失败处理器
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);

        //SecurityConfig允许跨域请求
        http.cors();
    }
11、SCRF

CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。

https://blog.csdn.net/freeking101/article/details/86537087

​ Spring Security去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。

​ 我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值