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小demo1、导包:
org.springframework.boot spring-boot-starter-security 2.6.7
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控制台,如下图所示:
Spring Security本质上是一个过滤器链,它的底层采用的是责任链的设计模式,它有一条很长的过滤器链,在启动类中随便写一行打印代码,然后打断点调试。点击下面计算器的图标,在输入框中输入:run.getBean(DefaultSecurityFilterChain.class)按回车就可查看15条过滤器链。
每一个过滤器功能具体见链接:
https://blog.csdn.net/K_520_W/article/details/1188552816、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 { List8、通过与数据库交互实现权限用户auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); return new User("wangwu", new BCryptPasswordEncoder().encode("123"), auths); } }
此处演示使用的是MyBatis-plus:
1)先导入MyBatis-plus、Mysql8.x、lombok依赖:
com.baomidou mybatis-plus-boot-starter 3.5.1 mysql mysql-connector-java 8.0.28 org.projectlombok lombok true
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 Listpermissions; public LoginUser(SysUser user, List permissions) { this.user = user; this.permissions = permissions; } //存储SpringSecurity所需要的权限信息的集合 @JSONField(serialize = false) private List 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{ }
SysUserService接口:
public interface SysUserService extends IService{ ResultOK login(SysUser user); }
SysUserServiceImpl实现类:
@Service public class SysUserServiceImpl extends ServiceImplimplements 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 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 { //根据用户名查询用户信息 QueryWrapperqueryWrapper = new QueryWrapper<>(); queryWrapper.eq("user_name", username); SysUser user = sysUserMapper.selectOne(queryWrapper); //如果查询不到数据就通过抛出异常来给出提示 if (Objects.isNull(user)) { throw new RuntimeException("用户名或密码错误!"); } //TODO 根据用户查询权限信息 添加到LoginUser中 //从数据库中查 List 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攻击也就不用担心了。