文章目录侵删!侵删!侵删!
- 基本概念介绍
- 介绍
- 认证
- 授权
- 项目搭建
- 内存认证
- 自定义认证逻辑
- 密码解析器PasswordEncoder
- 自定义登录界面
- 编写登录页面
- 在Spring Security配置类自定义登录页面
- 会话管理
- 认证成功后的处理方式
- 认证失败后的处理方式
- 退出登录
- 退出成功处理器
- 记住我
- 授权
- Role-Based Access Control
- Resource-Based Access Control
- 授权__权限表设计
- 编写查询权限的方法
- 配置类设置访问控制
- 自定义访问控制逻辑
- 注解设置访问控制
- 在前端进行访问控制
- 总结
源码:https://gitee.com/DoubleW2w/spring-security-learning
介绍SpringSecurity 安全服务框架,核心功能是认证和授权。提供了生命是安全访问控制功能,减少为了系统安全而编写大量重复代码的工作。
认证认证即判断用户的身份是否合法,合法则继续访问,不合法则拒绝访问。
- 用户密码登录,二维码登录,手机短信登录,人脸识别,指纹认证
目的:保护系统隐私数据与资源
授权认证通过后,根据用户的权限来控制用户访问资源的过程。普通用户只能看普通资源,VIP用户能看VIP资源。
认证是保证用户身份的合法性,授权则是为了更细粒度对数据进行划分,控制不同的用户能够访问不同的资源
项目搭建依赖pom文件
org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-security mysql mysql-connector-java runtime com.baomidou mybatis-plus-boot-starter 3.4.3.4 org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin
package com.doublew2w.controller;
@Controller
public class PageController {
@RequestMapping("/{page}")
public String showPage(@PathVariable String page) {
return page;
}
}
package com.doublew2w;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
resources.templates
登录首页
主页面
resources
server:
port: 80
#日志格式
logging:
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss, SSS} %clr(%-5level) --- [%thread] %cyan(%-50logger{50}):%msg%n'
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mysecurity?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
启动项目,访问项目主页面 http://localhost/main,项目会自动跳转到一个登录页面。这代表Spring Security已经开启了认证
功能,不登录无法访问所有资源,该页面就是 Spring Security 自带的登录页面。
我们使用 user 作为用户名,控制台中的字符串作为密码登录,登录成功后跳转到项目主页面。
内存认证Spring Security 会将登录页传来的用户名密码和内存中的用户名密码做匹配认证,使用到的类是 InMemoryUserDetailsManager
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
// 1.使用内存数据进行认证
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 2.创建两个用户
UserDetails user1 = User.withUsername("baizhan").password("123").authorities("admin").build();
UserDetails user2 = User.withUsername("sxt").password("456").authorities("admin").build();
// 3.将这两个用户添加到内存中
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
将登录页传来的用户名密码和内存中用户名密码做匹配认证。
自定义认证逻辑在实际项目中,认证逻辑是需要自定义控制的。将 UserDetailsService 接口的实现类放入Spring容器即可自定义认证逻辑
InMemoryUserDetailsManager 就是 UserDetailsService 接口的一个实现类,它将登录页传来的用户名密码和内存中用户名密码做匹配认证。当然我们也可以自定义 UserDetailsService 接口的实现类
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService 的实现类必须重写 loadUserByUsername() 方法,该方法定义了具体的认证逻辑。
参数 username 是前端传来的用户名,我们需要根据传来的用户名查询到该用户(一般是从数据库查询),并将查询到的用户封装成一个 UserDetails 对象,该对象是 Spring Security 提供的用户对象,包含用户名、密码、权限。Spring Security 会根据UserDetails 对象中的密码和客户端提供密码进行比较。相同则认证通过,不相同则认证失败。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
数据库建表
-- ---------------------------- -- Table structure for users -- ---------------------------- DROp TABLE IF EXISTS `users`; CREATE TABLE `users` ( `id` int(0) NOT NULL AUTO_INCREMENT, `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of users -- ---------------------------- INSERT INTO `users` VALUES (1, 'baizhan', 'baizhan', '13812345678'); INSERT INTO `users` VALUES (2, 'sxt', 'sxt', '13812345678'); INSERT INTO `users` VALUES (3, 'demo', '$2a$10$tBjD3Sdp6RGZdTlJoHns..3Yodf.DzahrkVHVoQGKSHCTxavCelHO', NULL);
编写实体类
@Data
public class Users {
private Integer id;
private String username;
private String password;
private String phone;
}
编写dao接口
public interface UsersMapper extends BaseMapper{ }
在 SpringBoot 启动类中添加 @MapperScan 注解,扫描Mapper文件夹
@SpringBootApplication
@MapperScan("com.doublew2w.mapper")
public class Application {
public static void main(String[] args)
{
SpringApplication.run(MysecurityApplication.class, args);
}
}
创建 UserDetailsService 的实现类,编写自定义认证逻辑
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
// 自定义认证逻辑
@Override
public UserDetails
loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.构造查询条件
QueryWrapper wrapper = new QueryWrapper().eq("username", username);
// 2.查询用户
Users users = usersMapper.selectOne(wrapper);
// 3.封装为UserDetails对象
UserDetails userDetails = User
.withUsername(users.getUsername())
.password(users.getPassword())
.authorities("admin")
.build();
// 4.返回封装好的UserDetails对象
return userDetails;
}
}
密码解析器PasswordEncoder
在数据库中存放密码时不会存放原密码,而是会存放加密后的密码。而用户传入的参数是明文密码。
Spring Security要求容器中必须有 PasswordEncoder 实例,之前使用的 NoOpPasswordEncoder 是 PasswordEncoder 的实现类,意思是不解析密码,使用明文密码。
Spring Security官方推荐的密码解析器是 BCryptPasswordEncoder 。
@SpringBootTest
public class PasswordEncoderTest {
@Test
public void testBCryptPasswordEncoder(){
//创建解析器
PasswordEncoder encoder = new BCryptPasswordEncoder();
//密码加密
String password = encoder.encode("baizhan");
System.out.println("加密后:"+password);
//密码校验
boolean result = encoder.matches("baizhan","$2a$10$/MImcrpDO21HAP2amayhme8j2SM0YM50/WO8YBH.NC1hEGGSU9ByO");
System.out.println(result);
}
}
将 BCryptPasswordEncoder 的实例放入Spring容器即可,并且在用户注册完成后,将密码加密再保存到数据库
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
自定义登录界面
Spring Security也支持用户自定义登录页面
编写登录页面 在Spring Security配置类自定义登录页面@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
//Spring Security配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义表单登录
http.formLogin().loginPage("https://blog.csdn.net/login.html")
// 自定义登录页面
.usernameParameter("username")
// 表单中的用户名项
.passwordParameter("password")
// 表单中的密码项
.loginProcessingUrl("/login")
// 登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
.successForwardUrl("/main")
//登录成功后跳转的路径
.failureForwardUrl("/fail");
//登录失败后跳转的路径
// 需要认证的资源
http.authorizeRequests()
.antMatchers("https://blog.csdn.net/login.html").permitAll()
// 登录页不需要认证
.anyRequest().authenticated();
//其余所有请求都需要认证
//关闭csrf防护
http.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
// 静态资源放行
web.ignoring().antMatchers("/css
private String pid;
private String permissionName;
private String url;
}
@Data
public class Role {
private String rid;
private String roleName;
private String roleDesc;
}
@Data
public class Users {
private Integer uid;
private String username;
private String password;
private String phone;
}
UsersMapper.java
public interface UsersMapper extends BaseMapper{ List findPermissionAllByUsername(@Param("username") String username); }
修改自认证逻辑
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
@Resource
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.构造查询条件
QueryWrapper wrapper = new QueryWrapper().eq("username", username);
//2.查询结果
Users users = usersMapper.selectOne(wrapper);
if (users == null) {
return null;
}
// 3.查询用户权限
List permissions = usersMapper.findPermissionAllByUsername(username);
// 4.将自定义权限集合转为Security的权限类型集合 List
List grantedAuthority = new ArrayList<>();
for (Permission permission : permissions) {
grantedAuthority.add(new SimpleGrantedAuthority(permission.getUrl()));
}
// 5.封装为UserDetails对象
UserDetails userDetails = User.withUsername(users.getUsername())
.password(users.getPassword())
.authorities(grantedAuthority)
.build();
// 6.返回对象
return userDetails;
}
}
配置类设置访问控制
由于没有权限,访问被拦截,就会抛出403异常。
编写控制器类,添加控制器方法资源
@RestController
public class MyController {
@GetMapping("https://blog.csdn.net/reportform/find")
public String findReportForm() {
return "查询报表";
}
@GetMapping("https://blog.csdn.net/salary/find")
public String findSalary() {
return "查询工资";
}
@GetMapping("https://blog.csdn.net/staff/find")
public String findStaff() {
return "查询员工";
}
}
修改Security配置类
// 权限拦截配置
http.authorizeRequests()
// 表示任何权限都可以访问
.antMatchers("https://blog.csdn.net/login.html").permitAll()
// 给资源配置需要的权限
.antMatchers("https://blog.csdn.net/reportform/find").hasAnyAuthority("https://blog.csdn.net/reportform/find")
.antMatchers("https://blog.csdn.net/salary/find").hasAnyAuthority("https://blog.csdn.net/salary/find")
.antMatchers("https://blog.csdn.net/staff/find").hasAnyAuthority("https://blog.csdn.net/staff/find")
//表示任何请求都需要认证后才能访问
.anyRequest().authenticated();
自定义访问控制逻辑
我们可以自定义访问控制逻辑,即访问资源时判断用户是否具有名为该资源URL的权限
自定义访问控制逻辑
@Service
public class MyAuthorizationService {
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
//获取会话中的登录用户
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
// 获取登录用户的权限
Collection extends GrantedAuthority> authorities = ((UserDetails) principal).getAuthorities();
// 获取请求的URL路径
String uri = request.getRequestURI();
// 将URL路径封装为权限对象
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(uri);
// 判断用户的权限集合是否包含请求的URL权限对象
return authorities.contains(authority);
}
return false;
}
}
在配置文件中使用自定义访问控制逻辑
// 权限拦截配置
http.authorizeRequests()
// 表示任何权限都可以访问
.antMatchers("https://blog.csdn.net/login.html").permitAll()
// 任何请求都使用自定义访问控制逻辑
.anyRequest().access("@myAuthorizationService.hasPermission(request,authentication)");
注解设置访问控制
除了配置类,在SpringSecurity中提供了一些访问控制的注解。这些注解默认都是不可用的,需要开启后使用。
@Secured 注解是基于角色的权限控制,要求 UserDetails 中的权限名必须以
ROLE_ 开头。
启动类
@SpringBootApplication
@MapperScan("com.doublew2w.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
在控制器上添加注解
@Secured("ROLE_reportform")
@GetMapping("https://blog.csdn.net/reportform/find")
public String findReportForm() {
return "查询报表";
}
不过该注解一般很少用,常用的是下面的注解 @PreAuthorize
// 权限拦截配置
http.authorizeRequests()
// 表示任何权限都可以访问
.antMatchers("https://blog.csdn.net/login.html").permitAll()
.antMatchers("/fail").permitAll()
.anyRequest().authenticated();
@PreAuthorize 注解可以在方法执行前判断用户是否具有权限
@SpringBootApplication
@MapperScan("com.doublew2w.mapper")
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@RestController
public class MyController {
@PreAuthorize("hasAnyAuthority('https://blog.csdn.net/reportform/find')")
@GetMapping("https://blog.csdn.net/reportform/find")
public String findReportForm() {
return "查询报表";
}
@PreAuthorize("hasAnyAuthority('https://blog.csdn.net/salary/find')")
@GetMapping("https://blog.csdn.net/salary/find")
public String findSalary() {
return "查询工资";
}
@PreAuthorize("hasAnyAuthority('https://blog.csdn.net/staff/find')")
@GetMapping("https://blog.csdn.net/staff/find")
public String findStaff() {
return "查询员工";
}
}
在前端进行访问控制
当没有对应权限的时候,就没显示该菜单
引入依赖
org.thymeleaf.extras thymeleaf-extras-springsecurity5
修改main.html
登录首页
主页面
退出登录
编写当返回403的时候,处理页面
权限不足
您的权限不足,请联系管理员!
编写权限不足处理类并配置
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.sendRedirect("/noPermission.html");
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Resource
private PersistentTokenRepository persistentTokenRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义表单登录
http.formLogin()
// 自定义登录页面
.loginPage("https://blog.csdn.net/login.html")
// 登录路径,表单向该路径提交,提交后自动执行UserDetailsService方法
.loginProcessingUrl("/login")
// 表单中的用户名项
.usernameParameter("username")
// 表单中的密码项
.passwordParameter("password")
// 登录成功处理器
.successHandler(new MyLoginSuccessHandler())
// 登录失败处理器
.failureHandler(new MyLoginFailureHandler());
// 关闭csrf防护
http.csrf().disable();
// 退出登录配置
http.logout()
// 登出路径
.logoutUrl("https://blog.csdn.net/logout")
// 登出成功后请求的页面
//.logoutSuccessUrl("https://blog.csdn.net/login.html")
.logoutSuccessHandler(new MyLogoutSuccessHandler())
//清除认证状 态,默认为true
.clearAuthentication(true)
// 销毁HttpSession对象,默认为true
.invalidateHttpSession(true);
// 记住我配置
http.rememberMe()
// 登录逻辑交给哪个对象
.userDetailsService(userDetailsService)
// 持久层对象
.tokenRepository(persistentTokenRepository)
// 保存时间(秒)
.tokenValiditySeconds(30);
// 权限拦截配置
http.authorizeRequests()
// 表示任何权限都可以访问
.antMatchers("https://blog.csdn.net/login.html").permitAll()
.antMatchers("/fail").permitAll()
.anyRequest().authenticated();
//异常处理
http.exceptionHandling()
.accessDeniedHandler(new MyAccessDeniedHandler());
}
@Override
public void configure(WebSecurity web) throws Exception {
// 静态资源放行
web.ignoring().antMatchers("/css
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
总结



