Spring Security之初体验

目录

目标

版本

初步集成Security

配置密码和账号权限

自定义登录页面

[通过Spring Security默认的表进行认证和授权](#通过Spring Security默认的表进行认证和授权)

自定义表进行认证和授权

设置密码加密

设定登录超时时间

隐藏登录页面


目标

将Spring Security集成到SpringBoot,通过配置给不同用户授权。本文适合初学者去了解Spring Security的基本应用,且部分内容只适用于前后不分离的项目


版本

<spring-boot.version>2.6.13</spring-boot.version>


初步集成Security

步骤

第一步:搭建SpringBoot项目。为了后面实现动态授权,我这里把MyBatis、MySQL也集成到了项目中,以下是Maven依赖。

XML 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

第二步 :给启动类加上**@EnableWebSecurity**注解。

java 复制代码
package com.ctx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

@SpringBootApplication
@EnableWebSecurity
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

第三步:配置application配置文件。

bash 复制代码
server.port=8010

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/school?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

mybatis.mapper-locations=classpath:/mapper/*.xml

第四步:写一个测试接口。

java 复制代码
package com.ctx.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {
    /**
     * http://localhost:8010/fun
     * @return
     */
    @GetMapping("/fun")
    public String fun(){
        return "fun";
    }

    /**
     * http://localhost:8010/fun2
     * @return
     */
    @GetMapping("/fun2")
    public String fun2(){
        return "fun2";
    }
}

第五步:启动项目并调用接口,发现跳转到了登录页面。

第六步:看控制台打印的信息,里面有user用户的密码,每次重启项目user用户的密码都会变化。


配置密码和账号权限

步骤

第一步:创建一个子类继承WebSecurityConfigurerAdapter类,作用是自定义web安全规则。

java 复制代码
package com.ctx.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭csrf跨域检查
        http.csrf().disable().authorizeRequests()
                //匹配路径 /fun/** 的请求,要求用户必须拥有 "fun" 权限
                .antMatchers("/fun/**").hasAuthority("fun")
                //匹配路径 /fun2/** 的请求,要求用户必须拥有 "fun2" 权限
                .antMatchers("/fun2/**").hasAuthority("fun2")
                //匹配路径 /common/** 的请求,允许所有用户访问,不需要登录
                .antMatchers("/common/**").permitAll()
                //所有其他请求都需要认证(登录)
                .anyRequest().authenticated()
                .and() //结束当前配置,继续下一个配置模块
                //配置表单登录成功后的默认跳转页面
                .formLogin().defaultSuccessUrl("/index.html")
                //登录失败后的跳转页面
                .failureUrl("/failure.html")
                .and()
                //权限不足则跳转到该页面
                .exceptionHandling().accessDeniedPage("/accessDenied.html");

    }
}

第二步:创建一个配置类实现WebMvcConfigurer,配置用户登录信息。

java 复制代码
package com.ctx.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login");
    }

    /**
     * PasswordEncoder认证时的密码比对
     * @return
     */
    @Bean
    public PasswordEncoder getPasswordEncoder() {
        //return new BCryptPasswordEncoder(10);
        //把用户输入的密码直接与正确的密码进行对比,没有加密解密过程。不推荐使用。
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 用户在表单中输入用户名和密码时,Spring Security会调用我们配置的UserDetailsService加载用户数据。
     * @return
     */
    @Bean
    public UserDetailsService getUserDetailsService() {
        return new InMemoryUserDetailsManager(
                //分别指用户、密码、角色、权限
                User.withUsername("zhangsan").password("123456").roles("teacher").authorities("fun").build(),
                User.withUsername("lisi").password("123456").roles("student").authorities("fun2").build(),
                User.withUsername("root").password("123456").roles("root").authorities("fun","fun2").build()
        );
    }
}

第三步:创建一些html静态页面,方便更直观地演示认证授权效果。

index.html

html 复制代码
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>登录成功 - 功能选择</title>
</head>
<body>
<h1>欢迎,已登录</h1>
<p>请选择要访问的功能:</p>

<button onclick="location.href='http://localhost:8010/fun'">访问功能 fun</button>
<button onclick="location.href='http://localhost:8010/fun2'">访问功能 fun2</button>
<button onclick="location.href='http://localhost:8010/logout'">退出登录</button>
</body>
</html>

failure.html

html 复制代码
<!-- failure.html -->
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>登录失败</title>
</head>
<body>
<h1 style="color:red;">登录失败</h1>
<p>请检查用户名和密码是否正确。</p>
<a href="/login">返回登录</a>
</body>
</html>

accessDenied.html

html 复制代码
<!-- 403.html -->
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>403 - 无权限访问</title>
</head>
<body>
<h1 style="color: red;">权限不足</h1>
<p>你没有权限访问该资源。</p>
<a href="/index.html">返回首页</a>
</body>
</html>

第四步:启动项目,分别用zhangsan、lisi、root去访问两个接口。发现每个账号的权限果真如我们配置的一般。


自定义登录页面

步骤

第一步:考虑到后面css、js、图片元素,所以我们重新规划好前端资源的目录结构。把我们之前的html配置页面前都加上/html前缀。

第二步:定义一个html首页。为了更好地演示授权控制,我们可以把首页做得复杂一点,加入css、js、图片元素。

homePage.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>登录</title>
  <link rel="stylesheet" href="/css/homePage.css" />
</head>
<body>
<div class="login-container">
  <img src="/img/logo.png" alt="Logo" class="logo"/>

  <!-- 使用 Spring Security 默认登录接口 -->
  <form action="/login" method="post">
    <h2>Login</h2>
    <div class="input-group">
      <label for="username">Username</label>
      <input type="text" id="username" name="username" required />
    </div>
    <div class="input-group">
      <label for="password">Password</label>
      <input type="password" id="password" name="password" required />
    </div>
    <button type="submit">Login</button>
  </form>

  <!-- 可选的错误消息提示 -->
  <p class="message" id="message">
    <!-- 你也可以用 Spring Security 的 /login?error 来显示错误 -->
    <!-- 可通过 JS 或 Thymeleaf 动态渲染错误 -->
  </p>
</div>

<script src="/js/homePage.js"></script>
</body>
</html>

homePage.css

html 复制代码
/* homePage.css */
body {
    margin: 0;
    padding: 0;
    font-family: Arial, sans-serif;
    background: linear-gradient(135deg, #4e54c8, #8f94fb);
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
}

.login-container {
    background: white;
    padding: 30px 40px;
    border-radius: 10px;
    box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
    text-align: center;
    width: 300px;
}

.logo {
    width: 80px;
    margin-bottom: 20px;
}

.input-group {
    margin-bottom: 15px;
    text-align: left;
}

.input-group label {
    display: block;
    font-weight: bold;
    margin-bottom: 5px;
}

.input-group input {
    width: 100%;
    padding: 8px;
    border: 1px solid #ccc;
    border-radius: 5px;
}

button {
    background: #4e54c8;
    color: white;
    padding: 10px;
    width: 100%;
    border: none;
    border-radius: 5px;
    font-size: 16px;
    cursor: pointer;
}

button:hover {
    background: #3b3fc1;
}

.message {
    margin-top: 15px;
    color: red;
}

homePage.js

javascript 复制代码
// script.js
document.getElementById("loginForm").addEventListener("submit", function (e) {
    e.preventDefault();

    const username = document.getElementById("username").value.trim();
    const password = document.getElementById("password").value.trim();
    const message = document.getElementById("message");

    if (username === "admin" && password === "123456") {
        message.style.color = "green";
        message.textContent = "Login successful!";
    } else {
        message.style.color = "red";
        message.textContent = "Invalid username or password.";
    }
});

logo.png

第三步:在WebSecurityConfig类中调整登录页面、登录页面权限、退出登录逻辑。

java 复制代码
package com.ctx.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 关闭 CSRF 跨域检查(适用于前端静态页面,生产建议打开)
        http.csrf().disable()
                .authorizeRequests()
                // 路径权限控制
                .antMatchers("/fun/**").hasAuthority("fun")
                .antMatchers("/fun2/**").hasAuthority("fun2")
                .antMatchers("/common/**", "/html/homePage.html", "/css/**", "/js/**", "/img/**").permitAll()
                .anyRequest().authenticated()
                .and()

                // 配置登录逻辑
                .formLogin()
                .loginPage("/html/homePage.html") // 指定登录页面(你自己的 HTML)
                .loginProcessingUrl("/login")     // Spring Security 登录接口(表单 action 的地址)
                .defaultSuccessUrl("/html/index.html") // 登录成功跳转
                .failureUrl("/html/failure.html")      // 登录失败跳转
                .and()

                // 配置退出逻辑
                .logout()
                .logoutUrl("/logout")                    // 默认就是 /logout,可以省略
                .logoutSuccessUrl("/html/homePage.html") // 退出成功跳转页面(你的登录页)
                .invalidateHttpSession(true)             // 注销 session
                .deleteCookies("JSESSIONID")             // 删除 cookie,确保彻底退出
                .and()

                // 权限不足跳转页面
                .exceptionHandling()
                .accessDeniedPage("/html/accessDenied.html");
    }
}

第四步:配置WebConfig类的addViewControllers,设置我们自定义的首页。

java 复制代码
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("forward:/html/homePage.html");
    }

第五步:启动项目并访问测试(略)。


通过Spring Security默认的表进行认证和授权

需求

生产环境中的账号和权限信息保存在数据库中,而非我们上述代码那样通过硬编码把账号和权限信息保存在内存中。接下来我们要把这些信息保存在数据库中,并实现认证和授权。

步骤

第一步:修改WebConfig类的getUserDetailsService方法。DataSource作为该方法的参数,Spring Boot会根据application配置文件把数据源注入进去。

java 复制代码
    @Bean
    public UserDetailsService getUserDetailsService(DataSource dataSource) {
        return new JdbcUserDetailsManager(dataSource);
    }

第二步 :创建Spring Security默认的两张表,我们可以在官方文档找到它。这里注意,要把varchar_ignorecase改成varchar类型

sql 复制代码
CREATE TABLE users(
	username VARCHAR(50) NOT NULL PRIMARY KEY,
	PASSWORD VARCHAR(500) NOT NULL,
	enabled BOOLEAN NOT NULL
);

CREATE TABLE authorities (
	username VARCHAR(50) NOT NULL,
	authority VARCHAR(50) NOT NULL,
	CONSTRAINT fk_authorities_users FOREIGN KEY(username) REFERENCES users(username)
);
CREATE UNIQUE INDEX ix_auth_username ON authorities (username,authority);

Spring Security JDBC身份验证https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/jdbc.html

第三步:往这两张表中写入数据。

sql 复制代码
-- 用户 zhangsan(角色 teacher,权限 fun)
INSERT INTO users (username, PASSWORD, enabled) VALUES
('zhangsan', '123456', TRUE);

INSERT INTO authorities (username, authority) VALUES
('zhangsan', 'ROLE_teacher'),
('zhangsan', 'fun');

-- 用户 lisi(角色 student,权限 fun2)
INSERT INTO users (username, PASSWORD, enabled) VALUES
('lisi', '123456', TRUE);

INSERT INTO authorities (username, authority) VALUES
('lisi', 'ROLE_student'),
('lisi', 'fun2');

-- 用户 root(角色 root,权限 fun, fun2)
INSERT INTO users (username, PASSWORD, enabled) VALUES
('root', '123456', TRUE);

INSERT INTO authorities (username, authority) VALUES
('root', 'ROLE_root'),
('root', 'fun'),
('root', 'fun2');

第四步:启动项目并测试(略)。


自定义表进行认证和授权

需求

项目中的用户和权限信息表往往和Spring Security默认的表不一样,请将项目中的用户和权限信息表与Spring Security进行整合,实现认证和授权功能。

分析

用户在表单中输入用户名和密码时,Spring Security会调用我们配置的UserDetailsService加载用户数据。我们之前的代码中,WebConfig类已经对UserDetailsService做了一些修改,并将它交给了Spring容器进行管理。我们进入到UserDetailsService内部,发现它是一个接口,并且只有一个方法。该方法的作用是:根据用户名查询并返回用户信息。我们可以实现这个接口,使得项目中的用户表与之关联。

第一步:自定义用户表和权限表。

sql 复制代码
CREATE TABLE `my_user` (
  `user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `user_password` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
sql 复制代码
CREATE TABLE `my_authorities` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `authority` varchar(50) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ix_auth_username` (`user_name`,`authority`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
sql 复制代码
CREATE UNIQUE INDEX ix_auth_username ON `my_authorities` (user_name,authority);

第二步:插入一些用户和权限数据。

sql 复制代码
INSERT INTO `my_user`(user_name,user_password)VALUES
("zhangsan","123456"),
("lisi","123456"),
("wangwu","123456"),
("root","123456")
sql 复制代码
INSERT INTO `my_authorities`(user_name,authority)VALUES
("zhangsan","fun"),
("zhangsan","ROLE_teacher"),

("lisi","fun2"),
("lisi","ROLE_student"),

("root","fun"),
("root","fun2"),
("root","ROLE_root")

第三步:实现UserDetailsService接口,并将我们刚才创建的两张表与之关联。

java 复制代码
package com.ctx.config;

import com.ctx.dao.MyUserDao;
import org.springframework.beans.factory.annotation.Autowired;
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.util.CollectionUtils;

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class MyUserService implements UserDetailsService {
    @Autowired
    private MyUserDao myUserDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Map<String, Object> userMap = myUserDao.loadUserByUsername(username);
        if (userMap == null) {
            throw new UsernameNotFoundException("用户不存在:" + username);
        }
        List<String> authorityList = myUserDao.selectAuthorityByUsername(username);
        //
        User.UserBuilder userBuilder = User.withUsername(username).password(userMap.get("userPassword").toString());
        //
        String[] array = authorityList.toArray(new String[authorityList.size()]);
        if (CollectionUtils.isEmpty(authorityList)) {
            throw new UsernameNotFoundException("用户:" + username+"没有任何权限。");
        }
        userBuilder.authorities(array);
        return userBuilder.build();
    }
}
java 复制代码
package com.ctx.dao;

import org.apache.ibatis.annotations.Param;

import java.util.List;
import java.util.Map;

@org.apache.ibatis.annotations.Mapper
public interface MyUserDao {

    Map<String, Object> loadUserByUsername(@Param("username") String username);

    List<String> selectAuthorityByUsername(@Param("username") String username);
}
XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.ctx.dao.MyUserDao">

    <select id="loadUserByUsername" resultType="java.util.Map">
        SELECT
            user_name userName,user_password userPassword,
        FROM
            my_user
        WHERE
            user_name = #{username}
    </select>
    <select id="selectAuthorityByUsername" resultType="java.util.Map">
        SELECT
            `authority`
        FROM
            `my_authorities`
        WHERE
            user_name = #{username}
    </select>
    
</mapper>
java 复制代码
package com.ctx.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.sql.DataSource;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("forward:/html/homePage.html");
    }

    /**
     * PasswordEncoder认证时的密码比对
     * @return
     */
    @Bean
    public PasswordEncoder getPasswordEncoder() {
        //return new BCryptPasswordEncoder(10);
        //把用户输入的密码直接与正确的密码进行对比,没有加密解密过程。不推荐使用。
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 用户在表单中输入用户名和密码时,Spring Security会调用我们配置的UserDetailsService加载用户数据。
     * @return
     */
    @Bean
    public UserDetailsService getUserDetailsService(DataSource dataSource) {
        return new MyUserService();
    }
}

第四步:启动项目并验证(略)。

扩展:org.springframework.security.core.userdetails.User中还有四个属性,这四个属性都要为true才能认证成功。

java 复制代码
package com.ctx.config;

import com.ctx.dao.MyUserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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.util.CollectionUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class MyUserService implements UserDetailsService {
    @Autowired
    private MyUserDao myUserDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Map<String, Object> userMap = myUserDao.loadUserByUsername(username);
        if (userMap == null) {
            throw new UsernameNotFoundException("用户不存在:" + username);
        }
        List<String> authorityList = myUserDao.selectAuthorityByUsername(username);
        //
        List<GrantedAuthority> authorities =null;
        if (CollectionUtils.isEmpty(authorityList)) {
            throw new UsernameNotFoundException("该账号没有任何权限:" + username);
        }else{
            authorityList.stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
        }
        UserDetails userDetails = User.withUsername(username)
                .password(userMap.get("userPassword").toString())
                .authorities(authorityList.toArray(new String[0]))
                .accountLocked(false)
                .accountExpired(false)
                .disabled(false)
                .credentialsExpired(false)
                .build();

        System.out.println("用户名: " + userDetails.getUsername());
        System.out.println("密码: " + userDetails.getPassword());
        System.out.println("是否启用: " + userDetails.isEnabled());
        System.out.println("是否账号未过期: " + userDetails.isAccountNonExpired());
        System.out.println("是否账号未锁定: " + userDetails.isAccountNonLocked());
        System.out.println("是否密码未过期: " + userDetails.isCredentialsNonExpired());
        System.out.println("权限列表: ");
        userDetails.getAuthorities().forEach(auth -> System.out.println(" - " + auth.getAuthority()));

        return userDetails;
    }
}

设置密码加密

需求

生产环境中,账号的密码是以一串密文的形式存储在数据库中的,修改刚才的认证系统,把数据库中的密码设置成密文存储。

分析

BCryptPasswordEncoder是Spring Security提供的密码加密工具,常用来对密码进行加密和匹配校验。它基于BCrypt哈希算法,安全性很好,推荐用来存储和验证密码。

第一步:创建加密方法,并对数据库中的密码进行加密,然后替换。

java 复制代码
    public static void main(String[] args) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

        // 加密密码
        String rawPassword = "123456";
        String encodedPassword = encoder.encode(rawPassword);
        System.out.println("加密后的密码:" + encodedPassword);

        // 验证密码
        boolean matches = encoder.matches(rawPassword, encodedPassword);
        System.out.println("密码匹配结果:" + matches);
    }

第二步:修改getPasswordEncoder方法的密码认证方式。

java 复制代码
    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

第三步:启动项目并测试(略)。


设定登录超时时间

需求

用户登录以后,在一定时间内可以访问权限内的资源,超时后需要重新登录才能继续访问。

方法一

第一步:在application文件中设定以下属性:

bash 复制代码
#设定登录超时时间是2分钟。注意:2m后面不要带空格
server.servlet.session.timeout=2m

方法二

第一步:实现AuthenticationSuccessHandler接口。

java 复制代码
package com.ctx.config;


import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@Component
public class MyAuthenticationSuccessHandler  implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(
            HttpServletRequest request,
            HttpServletResponse response,
            Authentication authentication
    )throws IOException, ServletException {
        // 登录成功后设置 session 超时时间
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.setMaxInactiveInterval(60); //秒
        }
        // 登录成功后跳转
        response.sendRedirect("/html/index.html");
    }
}

第二步:使上面的配置生效。这里注意:defaultSuccessUrl("/html/index.html")与response.sendRedirect("/html/index.html");冲突,需要把它注释掉。

java 复制代码
package com.ctx.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    // Spring自动注入,不需要写@Autowired
    public WebSecurityConfig(MyAuthenticationSuccessHandler myAuthenticationSuccessHandler) {
        this.myAuthenticationSuccessHandler = myAuthenticationSuccessHandler;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 关闭 CSRF 跨域检查(适用于前端静态页面,生产建议打开)
        http.csrf().disable()
                .authorizeRequests()
                // 路径权限控制
                .antMatchers("/fun/**").hasAuthority("fun")
                .antMatchers("/fun2/**").hasAuthority("fun2")
                .antMatchers("/common/**", "/html/homePage.html", "/css/**", "/js/**", "/img/**").permitAll()
                .anyRequest().authenticated()
                .and()

                // 配置登录逻辑
                .formLogin()
                .loginPage("/html/homePage.html") // 指定登录页面(你自己的 HTML)
                .loginProcessingUrl("/login")     // Spring Security 登录接口(表单 action 的地址)
                //.defaultSuccessUrl("/html/index.html") // 登录成功跳转
                .failureUrl("/html/failure.html")      // 登录失败跳转
                .successHandler(myAuthenticationSuccessHandler)
                .and()

                // 配置退出逻辑
                .logout()
                .logoutUrl("/logout")                    // 默认就是 /logout,可以省略
                .logoutSuccessUrl("/html/homePage.html") // 退出成功跳转页面(你的登录页)
                .invalidateHttpSession(true)             // 注销 session
                .deleteCookies("JSESSIONID")             // 删除 cookie,确保彻底退出
                .and()

                // 权限不足跳转页面
                .exceptionHandling()
                .accessDeniedPage("/html/accessDenied.html");
    }
}

隐藏登录页面

需求

用户登录后仍然有权限继续访问登录页面,这不符合操作习惯,需要改成:已登录继续访问登录页,则跳转到指定页面(一般都是跳转到登录成功后跳转的页面)。登录页面只有匿名用户可以访问

分析

在前后端分离的项目中,后端是无法感知到用户访问了哪些页面路径,页面由前端路由。访问页面不需要经过http请求,而是可以直接在浏览器中切换视图。而在前后不分离的项目中,后端是可以感知到具体路径的。

从以上分析中可以得出结论:在前后分离的项目中,后端项目不能控制用户访问登录页面,也不能禁止用户访问登录接口,只能决定用户访问登录接口后做出一系列反应:如果用户已登录则返回用户已登录的相关信息,未登录的用户则走项目的认证逻辑。

因此,针对这个需求(前后分离的项目中),后端必须做逻辑控制,前端推荐做逻辑控制

实现方法

因此如果项目前后不分离则使用anonymous()方法实现需求,反之通过过滤器配置只有匿名用户可以访问登录接口。前后分离项目中,我们可以自定义一个过滤器,该过滤器要先于UsernamePasswordAuthenticationFilter执行,这样才能避免已登录用户再次走认证逻辑。我们也可以继承UsernamePasswordAuthenticationFilter,在认证前判断是否是匿名用户,如果是匿名用户则走认证逻辑,否则返回用户已登录的相关信息。

备注

因为篇幅原因,这里只做前后不分离的实现案例。

第一步:在WebSecurityConfig中加如如下配置:

java 复制代码
                // 指定只有未登录用户可以访问登录页
                .antMatchers("/html/homePage.html").anonymous()

第二步:启动项目并测试(略)。

相关推荐
转码的小石1 个月前
深入Java大厂面试:从Spring框架到微服务架构的技术解析
java·spring boot·spring cloud·微服务·junit·spring security·hibernate
转码的小石1 个月前
深入Java面试:从Spring Boot到微服务
java·spring boot·kafka·spring security·oauth2
MyikJ2 个月前
Java 面试实录:从Spring到微服务的技术探讨
java·spring boot·微服务·kafka·spring security·grafana·prometheus
MyikJ2 个月前
Java互联网大厂面试:从Spring Boot到Kafka的技术深度探索
java·spring boot·微服务·面试·spark·kafka·spring security
述雾学java2 个月前
Spring Boot 整合 Spring Security
java·spring boot·spring security
Micro麦可乐2 个月前
最新Spring Security实战教程(十六)微服务间安全通信 - JWT令牌传递与校验机制
java·spring boot·安全·spring·spring cloud·微服务·spring security
Uranus^2 个月前
深入解析Spring Boot与Spring Security整合实现JWT认证
java·spring boot·spring security·认证·jwt