学学springSecurity的认证授权框架,后续整合一下JWT
先说一下基于RBAC权限管理的思路
- 在数据库中配置用户权限
- 在每个接口上使用注解的形式说明接口需要用户拥有那个权限才能访问
- SpringSecurity使用拦截请求的方式对当前用户拥有的权限和接口需要的权限进行比对,包含则放行,不包含则拦截
导入SpringSecurity的maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
SpringSecurity配置类
package com.example.springsecurity.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @program: spring-security
* @description:
* @author: fbl
* @create: 2020-12-01 14:40
**/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthenticationProvider provider; //注入我们自己的AuthenticationProvider
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭csrf防护
.csrf().disable()
.headers().frameOptions().disable()
.and();
http
//登录处理
.formLogin() //表单方式,或httpBasic
.loginPage("/loginPage")
.loginProcessingUrl("/form")
.defaultSuccessUrl("/index")
.failureUrl("/loginError") // 失败后跳转页面
.permitAll()
.and();
http
.authorizeRequests() // 授权配置
//无需权限访问
.antMatchers("/css/**", "/error404").permitAll()
.anyRequest().authenticated() // anyRequest 只能配置一个,多个会报错
// 添加自定义权限表达式
//.anyRequest().access("@rbacService.hasPermission(request,authentication)") //必须经过认证以后才能访问
//其他接口需要登录后才能访问
.and();
}
/**
* 自定义用户名和密码有2种方式,一种是在代码中写死 另一种是使用数据库
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 可以自己配置用户名 密码 角色 权限
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(provider);
}
}
注入我们自己的AuthenticationProvider(身份验证提供者)
package com.example.springsecurity.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* @program: spring-security
* @description:
* @author: fbl
* @create: 2020-12-01 15:17
**/
@Component
public class MyAuthenticationProvider implements AuthenticationProvider {
/**
* 注入我们自己定义的用户信息获取对象
*/
@Autowired
private MyUserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// TODO Auto-generated method stub
String userName = authentication.getName();// 这个获取表单输入中返回的用户名;
String password = (String) authentication.getCredentials();// 这个是表单中输入的密码;
// 这里构建来判断用户是否存在和密码是否正确
UserDetails userInfo = userDetailsService.loadUserByUsername(userName); // 这里调用我们的自己写的获取用户的方法;
if (userInfo == null) {
throw new BadCredentialsException("用户名不存在");
}
boolean flag = passwordEncoder.matches(password,userInfo.getPassword());
if (!flag) {
throw new BadCredentialsException("密码不正确");
}
Collection<? extends GrantedAuthority> authorities = userInfo.getAuthorities();
// 构建返回的用户登录成功的token
return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
// 这里直接改成retrun true;表示是支持这个执行
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
获取用户信息,并放入SpringSecurity缓存中(将来取出对比)
package com.example.springsecurity.config;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.springsecurity.mapper.UserMapper;
import com.example.springsecurity.model.SysUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @program: spring-security
* @description:
* @author: fbl
* @create: 2020-12-01 15:02
**/
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//这里可以可以通过username(登录时输入的用户名)然后到数据库中找到对应的用户信息,并构建成我们自己的UserInfo来返回。
//数据库中的密码是加密后的
QueryWrapper<SysUser> objectQueryWrapper = new QueryWrapper<>();
objectQueryWrapper.eq("user_name",username);
SysUser sysUser = userMapper.selectOne(objectQueryWrapper);
//由于权限参数不能为空,所以这里先使用AuthorityUtils.commaSeparatedStringToAuthorityList方法模拟一个admin的权限,该方法可以将逗号分隔的字符串转换为权限集合。
Set<String> permissions = new HashSet<>();
permissions.add(sysUser.getPermission());
return new User(username, sysUser.getPassword(), permissions.stream().map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()));
}
}
在启动类中注入SpringContextHolder
package com.example.springsecurity;
import com.example.springsecurity.utils.SpringContextHolder;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityApplication.class, args);
}
@Bean
public SpringContextHolder springContextHolder() {
return new SpringContextHolder();
}
}
接下来写Controller接口
package com.example.springsecurity.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @program: spring-security
* @description:
* @author: fbl
* @create: 2020-12-01 14:23
**/
@Controller
public class AuthController {
/**
* 跳转首页
*/
@GetMapping("")
public void index1(HttpServletResponse response){
//内部重定向
try {
response.sendRedirect("/index");
} catch (IOException e) {
e.printStackTrace();
}
}
@RequestMapping("/index")
@ResponseBody
public String index() {
return "登录成功";
}
@RequestMapping("/loginError")
@ResponseBody
public String loginError() {
return "登录失败";
}
@RequestMapping("/loginPage")
public String login() {
return "login_page";
}
/**
* 为了方便测试,我们调整添加另一个控制器 /whoim 的代码 ,让他返回当前登录的用户信息,
* 前面说了,他是存在SecurityContextHolder 的全局变量中,所以我们可以这样获取
*/
@RequestMapping("/whoim")
@ResponseBody
@PreAuthorize("@el.check('user','admin:list')")
public Object whoIm()
{
return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}
我们注意到有一个注解 @PreAuthorize("@el.check(‘user’,‘admin:list’)") 这个是说明这个接口需要user或者admin:list 权限才可以访问
@PreAuthorize是springSecurity提供的注解,表示在接口访问前拦截,需要在springSecurity的配置文件上注解 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) 生效。其中的参数是我们的自定义权限判定类
自定义权限判定类
/**
* Copyright (C) 2018-2020
* All rights reserved, Designed By www.yixiang.co
* 注意:
* 本软件为www.yixiang.co开发研制
*/
package com.example.springsecurity.service;
import com.example.springsecurity.utils.SpringContextHolder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 自定义判定权限
*/
@Service(value = "el")
public class ElPermissionConfig {
public Boolean check(String... permissions) {
// 获取当前用户的所有权限
List<String> elPermissions = getUserDetails().getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
// 判断当前用户的所有权限是否包含接口上定义的权限
return Arrays.stream(permissions).anyMatch(elPermissions::contains);
}
private UserDetails getUserDetails() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new RuntimeException("当前登录状态过期");
}
if (authentication.getPrincipal() instanceof UserDetails) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
UserDetailsService userDetailsService = SpringContextHolder.getBean(UserDetailsService.class);
return userDetailsService.loadUserByUsername(userDetails.getUsername());
}
throw new RuntimeException("找不到当前登录的信息");
}
}
写一个login_page.html页面为了登录(记得导入thymeleaf模板)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!DOCTYPE html>
<html id="ng-app" ng-app="app" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>home</title>
</head>
<body>
<form class="form-signin" action="/form" method="post">
<h2 class="form-signin-heading">用户登录</h2>
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username" class="form-control" placeholder="请输入用户名"/></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password" class="form-control" placeholder="请输入密码" /></td>
</tr>
<tr>
<td colspan="2">
<button type="submit" class="btn btn-lg btn-primary btn-block" >登录</button>
</td>
</tr>
</table>
</form>
</body>
</html>
我数据都是从数据库读取的,而且用的是myBatisPlus。
数据库表如下:
密码是经过BCryptPasswordEncoder加密的123
启动访问whoim接口,由于没有登录,跳到登录页面
输入 admin 123 成功返回出接口返回的信息
但是我把用户权限改一下
再次访问whoim接口
报错,权限不匹配