一、权限框架
1. 概述
权限管理是所有后台系统的都会涉及的一个重要组成部分,主要目的是对不同的人访问资源进行权限的控制,避免因权限控制缺失或操作不当引发的风险问题,如操作错误,隐私数据泄露等问题。那么如何在项目中实现权限呢?
使用权限框架Spring Security
市面上除了Spring Security,还有其他成熟的安全框架,比如Apache Shiro,但是在养老项目目前已经在使用Spring框架,那么Spring Security就是更好的选择。并且在使用市场占比上Spring Security也远超于Shiro
2. 核心概念
- 认证
大家可以简单的理解为:用户登录的行为就是认证(你是谁)
判断用户是否存在,判断用户密码是否正确
- 授权
授权就是在后管配置用户所在的角色是否有权限访问某些资源。
我们有很多的资源api列表,那这个登录后的用户所在的角色是否拥有这个api访问权限
- 鉴权:
用户通过认证并被授权后进行的一道安全检查,判断用户是否有权利执行或访问该资源(校验资源)
二. 快速入门
1、创建一个springboot项目
2、在pom文件引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
3、启动项目,到浏览器输入访问路径会出现以下页面
默认用户名是user,密码会输出在控制台,相当于目前在内存中写死了用户名和密码
输入用户名user(默认值)和密码后,会再次重定向访问到/hello的接口,spring security底层默认是使用session来维护登录态的,可以通过捉包观察请求头中携带的cookie信息
结论:
只要是集成了Spring Security框架之后,项目中的所有资源都得到了保护,必须经过认证之后才能访问,为什么只是引入了pom依赖之后就能保护程序呢?关键还是框架内容底层利用springboot自动配置原理,注入了一系列的过滤器、接口来实现的。
这里就完成了Spring Security的入门程序,其实Spring-Security其内部基础的处理方式就是通过过滤器来实现的,来我们看下刚才的例子用到的一些过滤器
下面是一些spring security过滤器,知道有就行,不需要记
过滤器 |
作用 |
WebAsyncManagerlntegrationFilter |
将WebAsyncManger与SpringSecurity上下文进行集成 |
SecurityContextPersistenceFilter |
在处理请求之前,将安全信息加载到SecurityContextHolder中 |
HeaderWriterFilter |
处理头信息假如响应中 |
CsrfFilter |
处理CSRF攻击 |
LogoutFilter |
处理注销登录 |
UsernamePasswordAuthenticationFilter |
处理表单登录 |
DefaultLoginPageGeneratingFilter |
配置默认登录页面 |
DefaultLogoutPageGeneratingFiltel |
配置默认注销页面 |
BasicAuthenticationFilter |
外理HttpBasic登录 |
RequestCacheAwareFilter |
处理请求缓存 |
SecurityContextHolderAwareRequestFilter |
包装原始请求 |
AnonvmousAuthenticationFilter |
配置匿名认证 |
SessionManagementFilter |
外理session并发问题 |
ExceptionTranslationFilter |
处理认证/授权中的异常 |
我们刚才的实现都是基于内存来构建用户的,在实际开发中,用户肯定会保存到数据库中,在Spring Security框架中提供了一个UserDetailsService接口,它的主要作用是提供用户详细信息。具体来说,当用户尝试进行身份验证时,UserDetailsService会被调用,以获取与用户相关的详细信息。这些详细信息包括用户的用户名、密码、角色等
三、项目集成案例:
上面基本都是理论知识,下面我们来看看通过数据库查询用户信息,进行自定义认证
1、执行流程图
新创建一个UserDetailsServiceImpl,让它实现UserDetailsService ,代码如下
1、
package com.itheima.project.config; 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.Component; public class UserDetailsServiceImpl implements UserDetailsService { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //查询数据库中的用户,并且返回框架要求的UserDetails return null; } }
- 当前对象需要让spring容器管理,所以在类上添加注解@Component
- 大家注意一下loadUserByUsername方法的返回值,叫做UserDetails,它有一个实现类也叫做User,如果正常返回这个对象且不为null,代表用户存在,如果抛出异常或者返回null,代表用户不存在,就不会进行后续校验密码的流程了
2. 数据库查询用户
我们下面就来实现数据库去查询用户,我们可以直接使用我们项目中的用户表,实现的步骤如下:
- 导入相关依赖(数据库、mybaits等)
- 添加配置:连接数据库、mybatis配置等(application.yml)
- 编写实体类和mapper
- 改造UserDetailsServiceImpl(用户从数据库中获取)
- pom文件添加依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.1</version> </dependency> <!--MySQL支持--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.19</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency>
3、application.yml添加数据库相关配置
#服务配置 server: #端口 port: 8080 spring: application: name: springsecurity-demo #数据源配置 datasource: druid: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.168.200.146:3306/security_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: heima123 # MyBatis配置 mybatis: #mapper配置文件,classpath不带*只会扫描当前工程项目,带*除了会扫描本项目工程,还会扫描第三方依赖 mapper-locations: classpath*:mapper*/*Mapper.xml type-aliases-package: com.itheima.project.entity configuration: # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 驼峰下划线转换 map-underscore-to-camel-case: true use-generated-keys: true default-statement-timeout: 60 default-fetch-size: 100
4、表结构及实体类和mapper
在中州养老虚拟机的数据库中新创建一个数据库,名字为:security_db
导入课程资料的权限基础代码中的sql文件夹的sys_user.sql脚本
用户实体类
package com.itheima.project.entity; import lombok.Data; public class User { public Long id; /** * 用户账号 */ private String username; /** * 密码 */ private String password; /** * 用户昵称 */ private String nickName; }
创建UserMapper接口,我们只需要定义一个根据用户名查询的方法即可
package com.itheima.project.mapper; import com.itheima.project.entity.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; /** * @author sjqn */ //注意这个@Mapper注解必加,因为本项目没有UserMapper.xml,也没有在启动类指定@MapperScan扫描路径,默认springboot启动会在启动类所在的包以及子包扫描@Mapper注解 public interface UserMapper { ("select * from sys_user where username = #{username}") public User findByUsername(String username); }
5、创建UserDetailsService的实现类UserDetailsServiceImpl
package com.itheima.project.config; import cn.hutool.core.util.ObjectUtil; import com.itheima.project.entity.User; import com.itheima.project.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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.Component; import java.util.ArrayList; import java.util.List; /** * @author sjqn */ public class UserDetailsServiceImpl implements UserDetailsService { private UserMapper userMapper; public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //查询用户 User user = userMapper.findByUsername(username); if(ObjectUtil.isEmpty(user)){ throw new RuntimeException("用户不存在"); } SimpleGrantedAuthority user_role = new SimpleGrantedAuthority("user"); SimpleGrantedAuthority admin_role = new SimpleGrantedAuthority("admin"); List<GrantedAuthority> list = new ArrayList<>(); list.add(user_role); list.add(admin_role); return new org.springframework.security.core.userdetails.User(user.getUsername() ,user.getPassword() , list); } }
6、测试
重启项目之后,可以根据数据库中有的用户进行登录,如果登录成功则表示整合成功
自定义UserDetails
上述代码中,返回的UserDetails或者是User都是框架提供的类,我们在项目开发的过程中,很多需求都是我们自定义的属性,我们需要扩展该怎么办?
其实,我们可以自定义一个类,来实现UserDetails,在自己定义的类中,就可以扩展自己想要的内容,如下代码:
package com.itheima.project.entity; import cn.hutool.core.util.ObjectUtil; import lombok.Data; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; public class UserAuth implements UserDetails { private String username; private String password; private String nickName; private List<String> roleList; public Collection<? extends GrantedAuthority> getAuthorities() { if(ObjectUtil.isEmpty(roleList)){ return null; } return roleList.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toList()); } public boolean isAccountNonExpired() { return true; } public boolean isAccountNonLocked() { return true; } public boolean isCredentialsNonExpired() { return true; } public boolean isEnabled() { return true; } }
然后,我们可以继续改造UserDetailsServiceImpl中检验用户的逻辑,代码如下:
package com.itheima.project.config; import cn.hutool.core.util.ObjectUtil; import com.itheima.project.entity.User; import com.itheima.project.entity.UserAuth; import com.itheima.project.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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.Component; import java.util.ArrayList; import java.util.List; /** * @author sjqn */ public class UserDetailsServiceImpl implements UserDetailsService { private UserMapper userMapper; public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //查询用户 User user = userMapper.findByUsername(username); if(ObjectUtil.isEmpty(user)){ throw new RuntimeException("用户不存在"); } UserAuth userAuth = new UserAuth(); userAuth.setUsername(user.getUsername()); userAuth.setPassword(user.getPassword()); userAuth.setNickName(user.getNickName()); //添加角色 List<String> roleList=new ArrayList<>(); //加入判断,方便后面授权的时候使用 if(ObjectUtil.equals("user@qq.com",username)){ roleList.add("USER"); } if(ObjectUtil.equals("admin@qq.com",username)){ roleList.add("USER"); roleList.add("ADMIN"); } userAuth.setRoleList(roleList); return userAuth; } }
根据执行流程,loadUserByUsername方法返回的UserDetails对象也会最终被包装到Authentication对象中的principal属性中一直往后传递,最后整个Authentication对象也会被放进去ThreadLocal中,所以接下里我们修改HelloController的hello方法,使用getPrincipal()方法读取认证自定义对象。
("/hello") public String hello(){ //认证成功,可以得到用户信息 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String name = authentication.getName(); //获取自定义属性 UserAuth principal = (UserAuth) authentication.getPrincipal(); return "hello "+name+" 昵称 "+principal.getNickName(); }
测试
重启项目之后,可以根据数据库中有的用户进行登录,并且能够正常显示用户昵称数据表示成功
1、授权和鉴权
授权的方式包括web授权和方法授权
- web授权是通过url拦截进行授权,基本用这种
- 方法授权是通过方法拦截进行授权,通过注解的方式控制权限,粒度小,耦合度高
2、web授权
下面让我们进行一个案例学习:
- 准备工作,修改HelloController,增加两个方法,主要是为了方便后边进行测试
("/hello/user") public String helloUser(){ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String name = authentication.getName(); return "hello-user "+name; } ("/hello/admin") public String helloAdmin(){ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String name = authentication.getName(); return "hello-admin "+name; }
- 新建一个 SecurityConfig配置类,实现方法,添加对以上两个地址的角色控制
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.formLogin() //自定义自己编写的登陆页面 .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/login") //登录访问路径 .permitAll()//登录页和登录访问路径无需登录也可以访问 .and() .authorizeRequests() .antMatchers("/css/**","/images/**").permitAll() .antMatchers("/hello/user").hasRole("USER") .antMatchers("/hello/admin").hasAnyRole("ADMIN")//hasAnyRole支持批量对某个url增加多个角色 .anyRequest().authenticated() .and() .csrf().disable(); //关闭csrf防护 return http.build(); }
目前这种资源和角色关系是写死在配置类中的,后续我们要改成从数据库中根据RBAC模型设计的表进行关联查询
- 测试
分别使用user和admin用户登录
- user用户可以访问
/hello/user - admin用户可以访问
/hello/user、/hello/admin
3、 整合JWT
目前我们学习的是Spring Security默认的流程认证,会自动完成一系列的认证动作,然后返回前端,所以我们没有办法在认证成功之后做一些扩展,比如想要生成jwt给前端,所以我们需要自己把控框架的认证流程,这块我们就需要拿到框架的一个东西叫做认证管理器AuthenticationManager,有了这个认证管理器,我们可以把控整个认证流程,把控制权牢牢掌握在我们手中,而要想实现这个效果,只需要将loginProcessingUrl方法配置成我们自己的登录接口url,不再使用框架默认提供的/login登录接口,或者不配置loginProcessingUrl方法也可以,然后另外定义一个不需要经过登录校验的接口即可,这就是自定义认证
1. 认证流程
2. 认证实现
环境准备
新建一个util包,然后写一个生成jwt的工具类,用于生成和验证JWT令牌
pom文件依赖
<!--JWT--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.1</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!--工具包--> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.75</version> </dependency>
3. 定义认证管理器
获取认证管理器AuthenticationManager,而认证管理器AuthenticationManager包装在认证配置类AuthenticationConfiguration中,所以配置如下:
/** * 认证管理器 * @param authenticationConfiguration * @return * @throws Exception */ public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); }
4. 登录功能实现
修改自定义配置,如果没有配置loginProcessingUrl方法,相当于开启了自定义认证的逻辑,不会走默认直接认证的逻辑,而且由于目前是接口开发,属于前后端分离,无需再使用自定义登录页面,只需要放行登录接口就行
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { //构建自定义配置 http.authorizeHttpRequests().antMatchers("/security/login").permitAll(); http.csrf().disable(); return http.build(); }
有了认证管理器之后,我们就可以在自己的登录接口中来使用认证管理器完成认证的操作
下面是自定义接口完成认证
package com.itheima.project.web; import com.itheima.project.entity.LoginDto; import com.itheima.project.entity.UserAuth; import com.itheima.project.util.JwtUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; /** * @author sjqn */ ("/security") public class LoginController { private AuthenticationManager authenticationManager; ("/login") public String login( LoginDto loginDto){ //包装用户的密码 UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(loginDto.getUsername(),loginDto.getPassword()); //需要一个认证管理器,来进行调用 Authentication authentication = authenticationManager.authenticate(upat); //判断认证是否成功 if(authenticate.isAuthenticated()){ //获取用户 UserAuth userAuth = (UserAuth) authentication.getPrincipal(); //认证成功 //生成jwt,返回 Map<String,Object> map = new HashMap<>(); map.put("user",userAuth); //过期时间单位为小时 String token = JwtUtil.createJWT("itcast", 24, map); return token; } return ""; } }
LoginDto
package com.itheima.project.entity; import lombok.Data; public class LoginDto { private String username; private String password; }
测试
使用ApiFox测试
- 输入正确的用户名和密码,则会返回jwt生成的token,也就说明认证成功了
4、鉴权管理器
当用户登录以后,携带了token访问后端,那么此时Spring Security框架就要对当前请求进行验证,验证包含了两部分,第一验证携带的token是否合法和是否过期,第二验证当前用户是否拥有当前访问资源的权限。
总体思路图
- 自定义授权管理器
我们创建一个类:TokenAuthorizationManager,这个类有两个要求
- 被spring容器进行管理,所以需要添加@Component注解
- 实现AuthorizationManager<RequestAuthorizationContext>接口,并重写check方法,在方法中编写校验逻辑
- 注意方法返回值为new AuthorizationDecision(true)或者null都相当于放行,返回值为new AuthorizationDecision(false)相当于返回403
package com.itheima.project.config; import cn.hutool.core.util.ObjectUtil; import cn.hutool.json.JSONUtil; import com.itheima.project.entity.UserAuth; import com.itheima.project.util.JwtUtil; import io.jsonwebtoken.Claims; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.access.intercept.RequestAuthorizationContext; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.function.Supplier; /** * @author sjqn */ public class TokenAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> { public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) { //获取request HttpServletRequest request = requestAuthorizationContext.getRequest(); //获取token String token = request.getHeader("token"); if(ObjectUtil.isEmpty(token)){ return new AuthorizationDecision(false); } //解析token Claims claims = null; try{ claims = JwtUtil.parseJWT("itcast", token); }catch(Exception e){ return new AuthorizationDecision(false); } if (ObjectUtil.isEmpty(claims)) { //token失效 return new AuthorizationDecision(false); } //获取userAuth UserAuth userAuth = JSONUtil.toBean(JSONUtil.toJsonStr(claims.get("user")), UserAuth.class); //存入上下文 UsernamePasswordAuthenticationToken auth =new UsernamePasswordAuthenticationToken(userAuth, userAuth.getPassword(), userAuth.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); //获取用户当前的请求地址 String requestURI = request.getRequestURI(); //判断地址与对象中的角色是否匹配 if(userAuth.getRoleList().contains("ADMIN")){ if("/hello/admin".equals(requestURI)){ return new AuthorizationDecision(true); } } if(userAuth.getRoleList().contains("USER")){ if("/hello/user".equals(requestURI)){ return new AuthorizationDecision(true); } } return new AuthorizationDecision(false); } }
- 注册鉴权管理器
鉴权管理器想要生效需要在SecurityConfig中进行注册才能使用
package com.itheima.project.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; /** * @author sjqn */ public class SecurityConfig { private TokenAuthorizationManager tokenAuthorizationManager; public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests().antMatchers("/security/login").permitAll() .anyRequest().access(tokenAuthorizationManager); //关闭session http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //关闭缓存 http.headers().cacheControl().disable(); http.csrf().disable(); //返回 return http.build(); } public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
4. 测试
重启项目之后可以采用apifox进行测试
- 测试一:
- 登录账号:user@qq.com 拥有角色:USER
- 可以访问:/hello/user
- 其他请求返回403
- 测试二:
- 登录账号:admin@qq.com 拥有角色:USER、ADMIN
- 可以访问:/hello/user、/hello/admin