目录
目标
将Spring Security集成到SpringBoot,通过配置给不同用户授权。本文适合初学者去了解Spring Security的基本应用,且部分内容只适用于前后不分离的项目。
版本
<spring-boot.version>2.6.13</spring-boot.version>
初步集成Security
步骤
第一步:搭建SpringBoot项目。为了后面实现动态授权,我这里把MyBatis、MySQL也集成到了项目中,以下是Maven依赖。
<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注解。
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配置文件。
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
第四步:写一个测试接口。
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安全规则。
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,配置用户登录信息。
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
<!-- 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
<!-- 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
<!-- 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
<!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
/* 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
// 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类中调整登录页面、登录页面权限、退出登录逻辑。
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,设置我们自定义的首页。
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("forward:/html/homePage.html");
}
第五步:启动项目并访问测试(略)。
通过Spring Security默认的表进行认证和授权
需求
生产环境中的账号和权限信息保存在数据库中,而非我们上述代码那样通过硬编码把账号和权限信息保存在内存中。接下来我们要把这些信息保存在数据库中,并实现认证和授权。
步骤
第一步:修改WebConfig类的getUserDetailsService方法。DataSource作为该方法的参数,Spring Boot会根据application配置文件把数据源注入进去。
@Bean
public UserDetailsService getUserDetailsService(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}
第二步:创建Spring Security默认的两张表,我们可以在官方文档找到它。这里注意,要把varchar_ignorecase改成varchar类型。
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);
第三步:往这两张表中写入数据。
-- 用户 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内部,发现它是一个接口,并且只有一个方法。该方法的作用是:根据用户名查询并返回用户信息。我们可以实现这个接口,使得项目中的用户表与之关联。
第一步:自定义用户表和权限表。
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
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
CREATE UNIQUE INDEX ix_auth_username ON `my_authorities` (user_name,authority);
第二步:插入一些用户和权限数据。
INSERT INTO `my_user`(user_name,user_password)VALUES
("zhangsan","123456"),
("lisi","123456"),
("wangwu","123456"),
("root","123456")
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接口,并将我们刚才创建的两张表与之关联。
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();
}
}
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 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>
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才能认证成功。
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哈希算法,安全性很好,推荐用来存储和验证密码。
第一步:创建加密方法,并对数据库中的密码进行加密,然后替换。
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方法的密码认证方式。
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
第三步:启动项目并测试(略)。
设定登录超时时间
需求
用户登录以后,在一定时间内可以访问权限内的资源,超时后需要重新登录才能继续访问。
方法一
第一步:在application文件中设定以下属性:
#设定登录超时时间是2分钟。注意:2m后面不要带空格
server.servlet.session.timeout=2m
方法二
第一步:实现AuthenticationSuccessHandler接口。
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");冲突,需要把它注释掉。
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中加如如下配置:
// 指定只有未登录用户可以访问登录页
.antMatchers("/html/homePage.html").anonymous()
第二步:启动项目并测试(略)。