目录
集成权限认证框架
权限认证基础知识
1.身份认证:
身份认证是计算机及软件系统用于确认用户具有合法身份的一种技术手段,计算机系统和计算机网络是一个虚拟的数字世界.在这个数字世界里,一切信息都是用一组特定的数据来表示的,计算机只能识别用户的数字身份,所有对用户的授权也是针对用户数字身份的授权,保证操作者的物理身份与数字身份相对应成为一个很重要的问题,身份认证技术的诞生就是为了解决这个问题;
目前Web应用的身份认证方式比较流行的有以下几种
- HTTP Form认证
- HTTP Basic认证
- HTTP Token认证
- HTTPS 证书认证
2.授权管理
授权管理的目的是为了控制资源的访问(哪些用户能够访问哪些资源,什么资源不能被访问),从而控制用户能够使用哪些系统资源,当用户登录后,系统会给用户分配系统资源访问权限,而这个权限是由管理员预先设定好的;
3.权限控制
只要有用户参与的系统一般都要有权限控制,权限控制实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源
授权管理和权限控制的区别
总的来说,授权管理的关注点在于确认用户的身份以及确定他们在系统中具有的权利,而权限控制的关注点则在定义和管理系统中用户或者实体的操作权限,以及确保只有授权的用户能够执行特定的操作
Spring Security
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
优缺点:
优点
- Spring Boot 官方提供了大量的非常方便的开箱即用的 Starter ,包括 Spring Security 的 Starter ,使得在 Spring Boot 中使用 Spring Security 变得更加容易。
- Spring Security功能强大,比较好用。
缺点
- Spring Security 是一个重量级的安全管理框架,
- Spring Security概念复杂,配置繁琐
Spring Boot集成SpringSecurity
一.不引入数据库的集成操作
1.引入依赖
注:此处的集成实例并未连接数据库,因此只有两个依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
引入security依赖后,默认所有的登录界面都被拦截到他的登录界面(访问路径为/login,退出为/logout),默认的账号为user,密码是SpringBoot启动时,控制台打印的
2.配置类
采用的自定义权限的方式进行配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder(){
// return NoOpPasswordEncoder.getInstance();//采用明文的方式,这里并不推荐
return new ECryptPasswordEncoder(); //采用加密的方式进行密码的配置
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String encoderPw=passwordEncoder().encode("123");
auth
.inMemoryAuthentication()
.withUser("zs") //用户名
.password(encoderPw) //用户密码
.authorities("book:list","book:add") //赋予的权限
.and()
.withUser("ls")
.password(encoderPw)
.authorities("book:list")
}
}
采用角色的方式进行控制
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder(){
return new ECryptPasswordEncoder(); //采用加密的方式进行密码的配置
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String encoderPw=passwordEncoder().encode("123");
auth
.inMemoryAuthentication()
.withUser("root")
.password("123456")
.roles("vip1", "vip2", "vip3")
.and()
.withUser("admin")
.password("123456")
.roles("vip1", "vip2")
.and()
.withUser("wang")
.password("123456")
.roles("vip1");
}
}
3.Controller
在controller控制层的方法上标注权限
@RestController
@RequestMapping("/books")
public class UserController {
@PreAuthorize("hasAuthority('book:list')")
@GetMapping("/hello")
public String hello(){
return "hello";
}
@PreAuthorize("hasAuthority('book:add')")
@GetMapping("/add")
public String add(){
return "add";
}
}
二.使用数据库进行集成
此处集成使用用户登录进行功能演示
1.引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
</dependencies>
2.配置类
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
private final TokenService tokenService;
private final ObjectMapper objectMapper;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//告诉security如何加载用户的认证信息,当用户进行身份验证时,security将使用制定的UserDetailsService检索用户信息
//需保证项目中有一个有效的UserDetailsService实现,此项目的实现类为UserDetailsServiceImpl
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭CSRF保护
/*
CSRF 是一种安全攻击,攻击者通过欺骗用户在受信任的网站上执行非预期的动作。
为了防范这种攻击,Spring Security 默认启用了 CSRF 保护机制。
这种机制会在表单中生成一个随机的令牌(CSRF token),并将其与用户的会话相关联。
当用户提交表单时,该令牌会被验证,以确保请求是由合法的用户发起的。
*/
http.csrf().disable();
/*
设置会话管理策略为SessionCreationPolicy.STATELESS
SessionCreationPolicy.STATELESS:禁用security对会话的管理
即不创建 HttpSession,也不使用任何会话状态。每个请求都被视为独立的,不依赖于之前的请求。
这种方式适用于无状态的身份验证机制,比如基于令牌的认证。
*/
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
/*
定义请求的授权规则
设置登录和注销不需要进行身份验证
*/
http.authorizeRequests()
.antMatchers("/login", "/logout").permitAll()
.anyRequest().authenticated();
// 登录失败处理
/*
http.exceptionHandling():配置异常的入口点
authenticationE`ntryPoint():身份验证失败的入口点
*/
http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
//设置状态码为401,表示未授权
response.setStatus(401);
//设置响应内容类型为JSON
response.setContentType("application/json;charset=UTF-8");
//将报错信息序列化为JSON对象并写入响应输出流
response.getWriter().write(objectMapper.writeValueAsString(AjaxResult.err(401, "用户名或密码错误")));
//关闭输出流
response.getWriter().close();
});
// 注销
http.logout().logoutSuccessHandler((request, response, authentication) -> {
//删除redis中的token
tokenService.removeToken(request.getHeader("token"));
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(AjaxResult.ok("注销成功")));
response.getWriter().close();
});
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
3.工具类
LoginUser类:自定义用户类,实现UserDetails接口,便于集成到security的认证和授权机制中
UserDetails接口:封装用户信息
@Data
//UserDetails:表示用户详细信息的接口.通常自定义的用户类都需要实现该接口,
//以便于能集成到security的认证和授权机制中
public class LoginUser implements UserDetails {
private User user; //用户
private Set<String> roleList; //角色集合
private Set<String> permsList; //权限集合
@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;
}
@Override //分配的权限集合
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
}
Token类:返回给前端的数据
@Data //自动生成getter和setter方法
@NoArgsConstructor //自动生成无参构造方法
@AllArgsConstructor //自动生成包含所有字段的有参构造方法
public class Token {
private String token; //用户编码
private String nickName; //用户昵称
}
tokenFilter:过滤器,前端每次发送请求时都会携带请求头,过滤器负责根据请求头将登录用户从redis提取出来并添加到security上下文中,进行权限控制和登录过期
public class TokenFilter extends OncePerRequestFilter{
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
if(StringUtils.hasText(token)){
LoginUser loginUser= tokenService.getLoginUser(token);
if(loginUser!=null){
UsernamePasswordAuthentcationToken auth = new UsernamePasswordAuthentcation(loginUser.getUsername(),null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
filterChain.doFilter(request,response);
}
}
4.Service
- TokenService接口,TokenServiceImpl实现类:实现对Token的一系列操作,此处只展示实现类
@Service
@RequiredArgsConstructor
public class TokenServiceImpl implements TokenService {
private final StringRedisTemplate stringRedisTemplate;
private final ObjectMapper objectMapper; //该对象用于将对象序列化为JSON格式或者反序列化JSON字符串为对象
@Override
@SneakyThrows //方法在不显式声明异常的情况下抛出异常
public Token createToken(LoginUser loginUser) {
String uuid = UUID.randomUUID().toString();
Token token = new Token(uuid,loginUser.getUsername());
//将对象序列化为JSON字符串并存入redis
stringRedisTemplate.opsForValue().set("security:login:"+uuid,objectMapper.writeValueAsString(loginUser), Duration.ofMinutes(10));
return token;
}
@Override
public boolean removeToken(String token) {
return stringRedisTemplate.delete("security:login:"+token);
}
@Override
public LoginUser getLoginUser() {
return null;
}
}
- UserDetailsServiceImpl类:实现UserDetailsService接口.
UserDetailsService接口:用于从数据源加载用户信息,该接口只有一个方法(loadUserByUsername),在该方法内部加载完用户信息后,返回一个实现了UserDetails接口的实现类
public class UserDetailsServiceImpl implements UserDetailsService {
private final IUserService userService;
private final IRoleService roleService;
private final IMenuService menuService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询单个用户信息
User user = userService.getOne(Wrappers.lambdaQuery(User.class).eq(User::getUserName, username));
//非空验证
if (user == null) throw new UsernameNotFoundException("用户名不存在");
//查询用户对应的角色
List<Role> roleList = roleService.listByUserId(user.getUserId());
//查询角色对应的权限
List<Menu> menuList = menuService.listByUserId(user.getUserId());
LoginUser loginUser = new LoginUser();
/*
roleList.stream():将roleList转化成一个流,这允许我对其中的元素进行各种操作
.map(Role::getRoleKey):使用map操作,将每个role对象映射为RoleKey的值,这创建了一个包含所有RoleKey的流
.collect(Collectors.toSet()):使用collect操作,将流中的元素收集到一个set集合中(set集合中的值是不能重复的)
*/
loginUser.setRoleList(roleList.stream().map(Role::getRoleKey).collect(Collectors.toSet()));
loginUser.setPermsList(menuList.stream().map(Menu::getPerms).collect(Collectors.toSet()));
loginUser.setUser(user);
//因为loginUser实现了UserDetails接口,因此可以将loinUser返回个Controller层
return loginUser;
}
}
5.Controller
loginController类
@RestController
@RequiredArgsConstructor
public class LoginController {
private final TokenService tokenService;
//AuthenticationManager类:身份验证管理器
private final AuthenticationManager authenticationManager;
@PostMapping("/login")
public AjaxResult login(User user,String userName) {
//将自定义User类转换为security所需要的UsernamePasswordAuthenticationToken对象
/*
UsernamePasswordAuthenticationToken:
security中用于封装用户名密码认证信息的一个类,实现了Authentication接口,用于表示一个认证请求
*/
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
//authenticate方法:处理登录请求
/*
security会自动调用实现了UserDetailService接口的类中的loadUserByUsername方法,
然后将实现了UserDetails接口的对象封装成一个Authentication
*/
Authentication authentication =authenticationManager.authenticate(authenticationToken);
//getPrincipal方法:提取用户信息
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Token token=tokenService.createToken(loginUser);
return AjaxResult.ok(token);
}
}