Spring Security是基于Spring的一套权限框架,它有两大重要核心功能:用户认证和用户授权。用户认证指的是某个用户能否访问该系统,用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般在系统中,不同的用户会分配不同的角色,不同的角色也对应不同的权限。
Spring Security 诞生于 2003 年年底,最先叫做“Spring的acegi安全系统”,在2006年的时候发布了1.0.0的版本,在2007年年底的时候正式成为Spring全家桶的一个成员,并更名为“Spring Security”。
Spring Security: 是Spring全家桶的一个部分,它功能强大,能很好的与Spring进行整合,但它是重量级的一个框架,和ssm相整合需要进行很多的繁琐配置(后期可详细研究进行哪些配置),Spring Boot 对于 Spring Security 提供了自动化的配置方案,可以使用更少的配置来使用Spring Security。
Shiro: 是Apache旗下的一款轻量级的权限框架,相对于Spring Security来说功能更少,和ssm相整合也没有Spring Security那么复杂。
1、导包:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
<version>2.6.7version>
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控制台,如下图所示:
Spring Security本质上是一个过滤器链,它的底层采用的是责任链的设计模式,它有一条很长的过滤器链,在启动类中随便写一行打印代码,然后打断点调试。点击下面计算器的图标,在输入框中输入:run.getBean(DefaultSecurityFilterChain.class)按回车就可查看15条过滤器链。
每一个过滤器功能具体见链接:
https://blog.csdn.net/K_520_W/article/details/118855281
1、UserDetailsService:用于查询数据库和密码,在SpringSecurity中,如果不连接数据库则自动分配一个user用户,密码随机生成,如果要连接数据库,则连接数据库并编写UserDetailsService实现类,数据库中的用户即为认证用户。
2、passwordEncoder:是Spring Security中的一个密码解析接口,其中有三个方法,如下图所示:
encode(): 是对字符串进行加密的方法。
matches():校验传入的明文密码rawPassword和加密密码encodedPassword是否相匹配。
upgradeEncoding() :此方法目前我还未用到过。
方案一:通过在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);
}
}
此处演示使用的是MyBatis-plus:
1)先导入MyBatis-plus、Mysql8.x、lombok依赖:
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.28version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
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
测试成功!
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";
}
@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();
}
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攻击也就不用担心了。