Thymeleaf

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

注意:

如果页面是静态页面,放在静态资源目录下;

如果页面是动态的,放在templates下,无法直接访问templates目录下的静态资源,必须由具体的控制器方法通过转发到该目录下指定的html页面。

关于静态资源目录相关

SpringBoot静态资源目录在哪里?

SpringBoot默认设置了静态资源路径,默认将:当前项目根路径+/+静态资源名 的访问映射到以下目录:

spring:
  web:
    resources:
      static-locations: classpath:/META-INF/resources/, classpath:/resources/, classpath:/static/, classpath:/public/, classpath:/templates/

如何修改SpringBoot默认的静态资源路径?

方法1:
spring:
  web:
    resources:
      static-locations:
 
方法2:
// 通过编写配置类实现WebMvcConfigurer的addResourceHandlers方法来完成
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/my/**") // 指的是对外暴露的访问路径 如果添加了拦截器配置,一定要保证该路径是放行的才能直接访问,否则也要被拦截判断
                .addResourceLocations("classpath:/dog/");// 指的是内部文件放置的目录,classpath目录在spring boot中指的是resources文件夹,
                //.addResourceLocations("file:H:\\image\\avatar\\");
            // 值得注意的是,配置的目录如果在classpath目录下,那么项目运行后,再往里面添加资源是看不到新添加的资源的,只有重启才能看见
            // 配置的目录在本地则没有影响
            // 当然,我们也可以选择在application.properties文件中通过spring.resources.static-locations=classpath:/haha/配置
    }
}

如何给静态资源添加访问前缀?

默认情况下,访问静态资源是无前缀的,即spring.mvc.static-path-pattern的默认值为:/**,如果添加了访问前缀,访问方式变为:当前项目根路径 + 访问前缀 + 静态资源名 例如:http://localhost.api/login.html 我们在访问任何静态资源时,都需要在根路径后面加上/api;

spring:
  mvc:
    static-path-pattern: /api/**

添加全局前缀

添加全局前缀意味着所有资源在访问时,都要带着这个全局前缀,不仅限于静态资源,包括控制器方法等。

server:
  servlet:
    context-path: /springboot

快速入门

页面演示

<!doctype html>
<html lang="ch" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title th:text="${title}">默认的title</title>
</head>
<body>
    <h2 th:text="${join}">默认的文本</h2>
</body>
</html>

1.编写控制器方法,转发到该页面,并为该页面的参数传值

 
@Controller
public class TestController {
    @GetMapping("/index")
    public String hello(Model model) {
        model.addAttribute("title","controller Title");
        model.addAttribute("join","controller Join");
        return "index";
    }
}

2.启动项目访问 http://localhost:8080/index

语法

引用命名空间

<html lang="ch" xmlns:th="http://www.thymeleaf.org">

基本使用方法:

thymeleaf中要动态传值,必须使用th:指定指令

引用web静态资源:

<script th:src="@{/js/boostrap.min.js}"></script>

访问model模型中的数据

<span th:text="${user.name}"></span>

数据遍历

<tr th:each="user : ${userlist}">
  <td th:text="${user.name}">tyrone</td>
  <td th:text="${user.age}">18</td>
</tr>

条件判断

<tr th:if="${messages.empty}">
    <td colspan="3">No messages</td>
</tr>

整合SpringSecurity

简介

Spring SecuritySpring项目组提供的安全服务框架,核心功能包括认证和授权。它为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。

  1. **身份认证:**Spring Security是Spring项目组提供的安全服务框架,核心功能包括认证和授权。它为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。
  2. **授权:**授权即认证通过后,根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。 比如在一些视频网站中,普通用户登录后只有观看免费视频的权限,而VIP用户登录后,网站会给该用户提供观看VIP视频的权限。

认证

登录认证流程

认证原理

SpringSecurity完整流程

SpringSecuity是一个过滤器链,包含了各种功能的过滤器。

UsernamePasswordAuthenticationFilter:负责处理在登录页面填写了用户名和密码后的登录请求。

ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeciedException和AuthenticationException。

FilterSecurityInterceptor:负责权限校验的过滤器。

Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。

AuthenticationManager接口:定义了认证Authentication的方法

UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。

UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

快速入门

依赖引入

<dependencies>
    <!--默认登录名是user,密码会打印在控制台-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

编写index.html页面及页面访问接口

<!doctype html>
<html lang="ch" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8"/>
    <title th:text="${title}">默认的title</title>
  </head>
  <body>
    <h2 th:text="${join}">默认的文本</h2>
  </body>
</html>
@Controller
public class TestController {
    @GetMapping("/index")
    public String hello(Model model) {
        model.addAttribute("title","controller Title");
        model.addAttribute("join","controller Join");
        return "index";
    }
}
启动项目后访问http://localhost:8080/index自动跳转到 http://localhost:8080/login 页面,这代表Spring

Security已经开启了认证功能,不登录无法访问所有资源,该页面就是Spring Security自带的登录页面。

内存认证

在实际开发中,用户数量不会只有一个,且密码是自己设置的。所以我们需要自定义配置用户信息。首先我们在内存中创建两个用户,Spring Security会将登录页传来的用户名密码和内存中用户名密码做匹配认证。

@Configuration
public class SecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        //1.使用内存数据验证
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        //2.创建两个用户
        UserDetails admin = User.withUsername("admin").password("admin").authorities("ROLE_ADMIN").build();
        UserDetails suadmin = User.withUsername("suadmin").password("suadmin").authorities("ROLE_ADMIN").build();
        //3.添加到内存中
        manager.createUser(admin);
        manager.createUser(suadmin);
 
        return manager;
    }
 
    //密码编辑器,不解析密码
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

UserDetailsService

在实际项目中,认证逻辑是需要自定义控制的。将UserDetailsService接口的实现类放入Spring容器即可自定义认证逻辑。InMemoryUserDetailsManager就是UserDetailsService接口的一个实现类,它将登录页传来的用户名密码和内存中用户名密码做匹配认证。当然我们也可以自定义UserDetailsService接口的实现类。

public interface UserDetailsService {
  UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
`UserDetailsService`的实现类必须重写`loadUserByUsername`方法,该方法定义了具体的认证逻辑,参数username是前端传来的用户名,我们需要根据传来的用户名查询到该用户(一般是从数据库查询),并将查询到的用户封装成一个`UserDetails`对象,该对象是`Spring Security`提供的用户对象,包含用户名、密码、权限。`Spring Security`会根据`UserDetails`对象中的密码和客户端提供密码进行比较。相同则认证通过,不相同则认证失败。

数据库认证

@Service
public class MyUserDetailsService implements UserDetailsService {
    //自定义认证类型
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        //1. 构造查询条件
        UserVo userVo = new UserVo();
        userVo.setUsername("admin");
        userVo.setPassword("admin1");
        //2. 判断数据库中是否存在该用户
        if (userVo == null){
            throw new UsernameNotFoundException("用户不存在");
        }
 
        //3. 封装为UserDetails对象
        UserDetails userDetails = User
                .withUsername(userVo.getUsername())
                .password(userVo.getPassword())
                .authorities("ROLE_ADMIN")
                .build();
        //4. 返回对象
        return userDetails;
    }
}

PasswordEncoder

在实际开发中,为了数据安全性,在数据库中存放密码时不会存放原密码,而是会存放加密后的密码。而用户传入的参数是明文密码。此时必须使用密码解析器才能将加密密码与明文密码做比对。Spring Security中的密码解析器是<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">PasswordEncoder</font>

Spring Security要求容器中必须有<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">PasswordEncoder</font>实例,之前使用的<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">NoOpPasswordEncoder</font><font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">PasswordEncoder</font>的实现类,意思是不解析密码,使用明文密码。

Spring Security官方推荐的密码解析器是<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">BCryptPasswordEncoder</font>

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

自定义登录页面

  1. 自定义登录页面文件 login.html;
  2. 在SpringSecurity配置类自定义登录页面;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    //Security详细配置
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        //自定义表单登录
        http.formLogin(form -> {
            form.loginPage("/login.html")//自定义登录页面
                    .usernameParameter("username")//表单中的用户名项
                    .passwordParameter("password")//表单中的密码项
                    .loginProcessingUrl("/login")//登录请求路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
                    .successForwardUrl("/index.html")//登录成功后跳转路径
                    .failureForwardUrl("/fail.html");//登录失败后跳转路径
        });
        //需要认证的资源
        http.authorizeRequests(resp -> {
            resp.antMatchers("/login.html", "/fail.html").permitAll();//不需要认证的资源
            resp.antMatchers("/CSS/**", "/js/**").permitAll();//静态资源不需要认证
            resp.anyRequest().authenticated();//其他资源都需要认证
        });
 
        //关闭csrf防护
        http.csrf(AbstractHttpConfigurer::disable);
        return http.build();
    }
}

CSRF防护

跨站请求伪造,通过伪造用户请求访问受信任的站点从而进行非法请求访问,是一种攻击手段。 Spring Security为了防止CSRF攻击,默认开启了CSRF防护,这限制了除了GET请求以外的大多数方法。我们要想正常使用Spring Security需要突破CSRF防护。

// 关闭csrf防护
http.csrf(csrf ->{
 csrf.disable();
});

认证成功后的处理方式

自定义登录成功处理器

public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {
  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    // 拿到登录用户的信息
    UserDetails userDetails = (UserDetails)authentication.getPrincipal();
    System.out.println("用户名:"+userDetails.getUsername());
    System.out.println("一些操作...");
 
 
    // 重定向到主页
    response.sendRedirect("/main");
   }
}

配置登录成功处理器

// 自定义表单登录
http.formLogin(form -> {
  form.loginPage("/login.html") // 自定义登录页面
     .usernameParameter("username") // 表单中的用户名项
     .passwordParameter("password") // 表单中的密码项
     .loginProcessingUrl("/login") //登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
    //           .successForwardUrl("/main.html")  //登录成功后跳转的路径
     .successHandler(new MyLoginSuccessHandler()) // 登录成功处理器
     .failureForwardUrl("/fail.html"); //登录失败后跳转的路径
});

认证失败后的处理方式

自定义登录失败处理器

public class MyLoginFailureHandler implements AuthenticationFailureHandler {
  @Override
  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
    System.out.println("记录失败日志...");
    response.sendRedirect("/fail.html");
   }
}

配置登录失败处理器

// 自定义表单登录
http.formLogin(form -> {
  form.loginPage("/login.html") // 自定义登录页面
     .usernameParameter("username") // 表单中的用户名项
     .passwordParameter("password") // 表单中的密码项
     .loginProcessingUrl("/login") //登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
    //           .successForwardUrl("/main.html")  //登录成功后跳转的路径
     .successHandler(new MyLoginSuccessHandler()) // 登录成功处理器
    //           .failureForwardUrl("/fail.html"); //登录失败后跳转的路径
     .failureHandler(new MyLoginFailureHandler()); // 登录失败处理器
});

退出登录

退出登录后,Spring Security可以进行以下操作:

  • 清除认证状态
  • 销毁HttpSession对象
  • 跳转到登录页面

在Spring Security中,退出登录的写法如下:

http.logout(logout -> {
  logout.logoutUrl("/logout") // 退出登录路径
     .logoutSuccessUrl("/login.html") // 退出登录后跳转的路径
     .clearAuthentication(true) //清除认证状态,默认为true
     .invalidateHttpSession(true); // 销毁HttpSession对象,默认为true
});

退出成功处理器

自定义退出成功处理器

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
  @Override
  public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    System.out.println("清除一些数据...");
    response.sendRedirect("/login.html");
   }
}

配置退出成功处理器

// 退出登录
http.logout(logout ->{
  logout.logoutUrl("/logout") // 退出登录路径
    //           .logoutSuccessUrl("/login.html") // 退出登录后跳转的路径
     .logoutSuccessHandler(new MyLogoutSuccessHandler()) // 自定义退出成功处理器
     .clearAuthentication(true) // 清除认证状态,默认为true
     .invalidateHttpSession(true); // 销毁HttpSession对象,默认为true
});

Remember Me(记住我)

Spring Security中Remember Me为“记住我”功能,即下次访问系统时无需重新登录。当使用“记住我”功能登录后,Spring Security会生成一个令牌,令牌一方面保存到数据库中,另一方面生成一个叫remember-me的Cookie保存到客户端。之后客户端访问项目时自动携带令牌,不登录即可完成认证。

编写“记住我”配置类

@Configuration
public class RememberMeConfig {
  @Autowired
  private DataSource dataSource;
 
 
  // 令牌Repository
  @Bean
  public PersistentTokenRepository getPersistentTokenRepository() {
    // 为Spring Security自带的令牌控制器设置数据源
    JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl = new JdbcTokenRepositoryImpl();
    jdbcTokenRepositoryImpl.setDataSource(dataSource);
    //自动建表,第一次启动时需要,第二次启动时注释掉
//     jdbcTokenRepositoryImpl.setCreateTableOnStartup(true);
    return jdbcTokenRepositoryImpl;
   }
}

修改配置类

// 记住我配置
http.rememberMe(remember -> {
  remember.userDetailsService(userDetailsService) //认证逻辑对象
     .tokenRepository(repository) //持久层对象
     .tokenValiditySeconds(30); //保存时间,单位:秒
});

在登录页面添加“记住我”复选框

<form class="form" action="/login" method="post">
  <input type="text" placeholder="用户名" name="username">
  <input type="password" placeholder="密码" name="password">
  <input type="checkbox" name="remember-me" value="true"/>记住我</br>
  <button type="submit">登录</button>
</form>

会话管理

用户认证通过后,有时我们需要获取用户信息,比如在网站顶部显示:欢迎您,XXX。Spring Security将用户信息保存在会话中,并提供会话管理,我们可以从<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">SecurityContext</font>对象中获取用户信息,SecurityContext对象与当前线程进行绑定。

获取用户信息

@RestController
public class MyController {
    // 获取当前登录用户名
    @RequestMapping("/users/username")
    public String getUsername(){
        // 1.获取会话对象
        SecurityContext context = SecurityContextHolder.getContext();
        // 2.获取认证对象
        Authentication authentication = context.getAuthentication();
        // 3.获取登录用户信息
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
 
        return userDetails.getUsername();
    }
}

会话失效处理

会话过期是指当用户登录网站后,较长一段时间没有与服务器进行交互,将会导致服务器上的用户会话数据(即session)被销毁。此时,当用户再次操作网页时,服务器会进行session校验,浏览器提醒用户session超时。此时相当于用户被动的退出登录。

配置会话失效时间

server:
  servlet:
   session:
   # 会话过期时间默认是30m过期
    timeout: 30s

配置会话失效策略

public class MyInvalidSessionStrategy implements InvalidSessionStrategy {
  @Override
  public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
    System.out.println("会话过期");
    // 会话失效,需要创建新session,否则会由于一直没有session不断的重定向
    request.getSession();
    response.sendRedirect("/login");
   }
}

配置自定义过期策略

// 会话配置
http.sessionManagement(session ->{
  // 会话失效跳转的页面
  //       session.invalidSessionUrl("/login");
  // 会话失效处理器
  session.invalidSessionStrategy(new MyInvalidSessionStrategy());
});

会话并发控制

同一个系统中,只允许一个用户在一个终端上登录,这就是对会话的并发控制。有两种处理策略:

策略1:踢掉原有登录用户

通过**<font style="color:rgb(77, 77, 77);">maximumSessions()</font>**方法来置单个用户允许同时在线的最大并发会话数,如果没有额外配置,重新登录的会话会踢掉旧的会话。

// 会话配置
http.sessionManagement(session ->{
  // 会话失效跳转的页面
  //       session.invalidSessionUrl("/login");
  // 会话失效处理器
  session.invalidSessionStrategy(new MyInvalidSessionStrategy());
   //最大并发会话数,设置单个用户允许同时在线的最大会话数,重新登录的会话会踢掉旧的会话.
  session.maximumSessions(1);
});

策略2:阻止新用户登录

// 会话配置
http.sessionManagement(session ->{
  // 会话失效跳转的页面
  //       session.invalidSessionUrl("/login");
  // 会话失效处理器
  session.invalidSessionStrategy(new MyInvalidSessionStrategy());
  //当会话达到最大值时,是否保留已经登录的用户,默认为false
  session.maximumSessions(1).maxSessionsPreventsLogin(true);
});

主动踢人下线

在容器中注入SessionRegistry

// 注入SessionRegistry
@Bean
public SessionRegistry sessionRegistry(){
  return new SessionRegistryImpl();
}

编写踢出用户的方法

@Autowired
private SessionRegistry sessionRegistry;
 
// 踢出指定用户
@GetMapping("/kickOut")
public void kickOutUser(String username) {
  // 1.获取全部登录用户
  List<Object> allPrincipals = sessionRegistry.getAllPrincipals();
  // 2.遍历全部登录用户,找到要强制登出的用户
  for (Object principal : allPrincipals) {
    UserDetails userDetail = (UserDetails) principal;
    if (username.equals(userDetail.getUsername())) {
      // 3.找到认证用户所有的会话,不包含过期会话
      List<SessionInformation> sessions = sessionRegistry.getAllSessions(userDetail, false);
      if (null != sessions && !sessions.isEmpty()) {
        // 4.遍历该用户的会话,使其立即失效
        for (SessionInformation session : sessions) {
          session.expireNow();
         }
       }
     }
   }
}

RBAC

授权即认证通过后,系统给用户赋予一定的权限,用户只能根据权限访问系统中的某些资源。RBAC是业界普遍采用的授权方式,它有两种解释:

Role-Based Access Control

基于角色的访问控制,即按角色进行授权。比如在企业管理系统中,主体角色为总经理可以查询企业运营报表。逻辑为:

if(主体.hasRole("总经理角色")){ 
    查询运营报表
}

如果查询企业运营报表的角色变化为总经理和股东,此时就需要修改判断逻辑代码:

if(主体.hasRole("总经理角色") || 主体.hasRole("股东角色")){ 
    查询运营报表
}

Resource-Based Access Control

基于资源的访问控制,即按资源(或权限)进行授权。比如在企业管理系统中,用户必须 具有查询报表权限才可以查询企业运营报表。逻辑为:

if(主体.hasPermission("查询报表权限")){ 
    查询运营报表
}

这样在系统设计时就已经定义好查询报表的权限标识,即使查询报表所需要的角色变化为总经理和股东也不需要修改授权代码,系统可扩展性强。该授权方式更加常用。

权限表设计

用户和权限的关系为多对多,即用户拥有多个权限,权限也属于多个用户,所以建表方式如下:

这种方式需要指定用户有哪些权限,如:张三有查询工资的权限,即在用户权限中间表中添加一条数据,分别记录张三和查询工资权限ID。但在系统中权限数量可能非常庞大,如果一条一条添加维护数据较为繁琐。所以我们通常的做法是再加一张角色表:

用户角色,角色权限都是多对多关系,即一个用户拥有多个角色,一个角色属于多个用户;一个角色拥有多个权限,一个权限属于多个角色。这种方式需要指定用户有哪些角色,而角色又有哪些权限。

403处理方案

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>权限不足</title>
</head>
<body>
<h1>您的权限不足,请联系管理员!</h1>
</body>
</html>

编写权限不足类

public class MyAccessDeniedHandler implements AccessDeniedHandler {
  @Override
  public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    response.sendRedirect("/noPermission.html");
   }
}

配置权限异常

//异常处理
http.exceptionHandling().
        accessDeniedHandler(new MyAccessDeniedHandler());