前言

本文不着重讲述SpringSecurity相关概念及其原理,而是致力于总结一些实战应用场景

基于数据库的登录认证

1.创建数据库表

-- 创建数据库
CREATE DATABASE `security-demo`;
USE `security-demo`;

-- 创建用户表
CREATE TABLE `user`(
	`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
	`username` VARCHAR(50) DEFAULT NULL ,
	`password` VARCHAR(500) DEFAULT NULL,
	`enabled` BOOLEAN NOT NULL
);
-- 唯一索引
CREATE UNIQUE INDEX `user_username_uindex` ON `user`(`username`); 

-- 插入用户数据(密码是 "abc" )
INSERT INTO `user` (`username`, `password`, `enabled`) VALUES
('admin', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Helen', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Tom', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE);

2.创建实体类

@Data
public class User {

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    private String username;

    private String password;

    private Boolean enabled;

}

3.定义DBUserDetailsManager

DBUserDetailsManager要实现UserDetailsManager和UserDetailsPasswordService并重写其抽象方法。


@Component  
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {  
  
    @Autowired  
    private UserService userService;  
  
  
    @Override  
    public UserDetails updatePassword(UserDetails user, String newPassword) {  
        return null;  
    }  
  
    /**  
	 * 根据用户名加载用户详情  
	 * @param username 用户名  
	 * @return 用户详情  
	 * @throws UsernameNotFoundException 如果用户不存在  
	 */  
	@Override  
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {  
	    User user = userService.lambdaQuery().eq(User::getUsername, username).one();  
	    if (user == null) {  
	        throw new UsernameNotFoundException("用户不存在");  
	    }  
    // 返回自定义的安全用户对象,将数据库实体包装在内  
    return new SecurityUser(user);  
	}
  
    @Override  
    public void createUser(UserDetails user) {  
  
    }  
  
    @Override  
    public void updateUser(UserDetails user) {  
  
    }  
  
    @Override  
    public void deleteUser(String username) {  
  
    }  
  
    @Override  
    public void changePassword(String oldPassword, String newPassword) {  
  
    }  
  
    @Override  
    public boolean userExists(String username) {  
        return false;  
    }  
}

4.编辑配置

配置要根据项目实际情况进行选择,如果不主动实现filterChain,框架也会给出一个默认实现

@Configuration  
@EnableMethodSecurity // 开启方法级权限控制  
public class WebSecurityConfig {  
  
    @Bean  
    public PasswordEncoder passwordEncoder() {  
        return new BCryptPasswordEncoder();  
    }  
  
    @Bean  
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {  
        http  
            // 1. 配置请求授权  
            .authorizeHttpRequests(authorize -> authorize  
                    .requestMatchers("/user/add", "/login", "/error").permitAll() // 明确放行注册、登录和错误页  
                    .requestMatchers("/static/**", "/css/**", "/js/**").permitAll() // 放行静态资源  
                    .anyRequest().authenticated() // 剩下的所有请求都需要登录  
            )  
  
            // 2. 自定义表单登录逻辑  
            .formLogin(form -> form  
                    .defaultSuccessUrl("/", true) // 登录成功后强制跳转到首页  
                    .permitAll()  
            )  
  
            // 3. 配置注销功能  
            .logout(logout -> logout  
                    .logoutUrl("/logout") // 注销接口地址  
                    .logoutSuccessUrl("/login?logout") // 注销成功后跳转回登录页并带上提示  
                    .invalidateHttpSession(true) // 销毁 Session                    .deleteCookies("JSESSIONID") // 删除 Cookie                    .permitAll()  
            )  
  
            // 4. 防护配置  
            .csrf(csrf -> csrf.disable()); // Demo 阶段保持禁用以便 API 测试  
  
        return http.build();  
    }  
  
}

密码加密

1.配置passwordEncoder

config中添加PasswordEncoder,指明BCrypt加密算法

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

2.加密密码

passwordEncoder.encode(user.getPassword())

添加用户

1.自定义SecurityUser 类

自定义一个 SecurityUser 类作为包装类既能保持实体类 User 的纯粹性(不依赖 Spring Security 框架),也能在安全上下文中携带完整的业务数据。

public class SecurityUser implements UserDetails {  
  
    @Getter  
    private final User user; // 持有原始的数据库实体对象  
  
    public SecurityUser(User user) {  
        this.user = user;  
    }  
  
    @Override  
    public String getUsername() {  
        return user.getUsername();  
    }  
  
    @Override  
    public String getPassword() {  
        return user.getPassword();  
    }  
  
    @Override  
    public Collection<? extends GrantedAuthority> getAuthorities() {  
        // 后续可以在此根据 user 角色添加权限  
        return new ArrayList<>();  
    }  
  
    @Override  
    public boolean isAccountNonExpired() { return true; }  
  
    @Override  
    public boolean isAccountNonLocked() { return true; }  
  
    @Override  
    public boolean isCredentialsNonExpired() { return true; }  
  
    @Override  
    public boolean isEnabled() {  
        return user.getEnabled() != null && user.getEnabled();  
    }  
}

2.重写DBUserDetailsManager中方法

@Override  
public void createUser(UserDetails userDetails) {  
    if (userDetails instanceof SecurityUser) {  
        // 如果是自定义类型,直接获取内部持有实体保存  
        userService.save(((SecurityUser) userDetails).getUser());  
    } else {  
        // 兜底逻辑:处理标准的 UserDetails 对象  
        User user = new User();  
        user.setUsername(userDetails.getUsername());  
        user.setPassword(userDetails.getPassword());  
        user.setEnabled(userDetails.isEnabled());  
        userService.save(user);  
    }  
}  
  
@Override  
public void updateUser(UserDetails user) {  
    // 根据业务需求实现,例如:  
    // userService.updateById(convertToEntity(user));  
}  
  
@Override  
public void deleteUser(String username) {  
    userService.lambdaUpdate().eq(User::getUsername, username).remove();  
}  
  
@Override  
public void changePassword(String oldPassword, String newPassword) {  
  
}  
  
@Override  
public boolean userExists(String username) {  
    return userService.lambdaQuery().eq(User::getUsername, username).exists();  
}

3.UserController中实现接口并调用已重写的方法

@RequiredArgsConstructor  
@RestController  
@RequestMapping("/user")  
public class UserController {  
  
    private final UserService userService;  
    private final UserDetailsManager userDetailsManager;  
    private final PasswordEncoder passwordEncoder;  
  
    @PostMapping("/add")  
    public String add(@RequestBody User user) {  
        // 1. 校验用户是否存在  
        if (userDetailsManager.userExists(user.getUsername())) {  
            return "添加失败,用户名已存在";  
        }  
  
        // 2. 加密密码  
        user.setPassword(passwordEncoder.encode(user.getPassword()));  
  
        // 3. 使用自定义的 SecurityUser 包装实体类  
        SecurityUser securityUser = new SecurityUser(user);  
  
        // 4. 通过标准接口保存用户  
        try {  
            userDetailsManager.createUser(securityUser);  
            return "添加成功";  
        } catch (Exception e) {  
            log.error("添加用户失败", e);  
            return "添加失败:" + e.getMessage();  
        }  
    }  
}

前后端响应

在用户登录成功,失败或是注销时返回给前端对应的JSON数据(此处为了方便演示,把统一响应结果封装在Map集合中,实际开发还是以自定义结果对象为好)

登录成功

编写实现了AuthenticationSuccessHandler的类,用户登录成功时会调用该类中的onAuthenticationSuccess方法

@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        //获取用户身份信息
        Object principal = authentication.getPrincipal();

        //创建结果对象
        HashMap result = new HashMap();
        result.put("code", 0);
        result.put("message", "登录成功");
        result.put("data", principal);

        //转换成json字符串
        String json = JSON.toJSONString(result);

        //返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}

登陆失败

编写实现了AuthenticationFailureHandler的类,用户登录失败时会调用该类中的onAuthenticationFailure方法

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        //获取错误信息
        String localizedMessage = exception.getLocalizedMessage();

        //创建结果对象
        HashMap result = new HashMap();
        result.put("code", -1);
        result.put("message", localizedMessage);

        //转换成json字符串
        String json = JSON.toJSONString(result);

        //返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}

注销

编写实现了LogoutSuccessHandler的类,用户注销时会调用该类中的onLogoutSuccess方法

@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        //创建结果对象
        HashMap result = new HashMap();
        result.put("code", 0);
        result.put("message", "注销成功");

        //转换成json字符串
        String json = JSON.toJSONString(result);

        //返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}

挂载

目前位置以上三个handler只是进行了实现,但是为被挂载使用,挂载方法:

@Configuration  
@EnableMethodSecurity // 开启方法级权限控制  
public class WebSecurityConfig {  
  
    private final MyAuthenticationSuccessHandler successHandler;  
    private final MyLogoutSuccessHandler logoutSuccessHandler;  
    private final MyAuthenticationFailureHandler failureHandler;  
  
    // 构造器注入  
    public WebSecurityConfig(MyAuthenticationSuccessHandler successHandler, MyLogoutSuccessHandler logoutSuccessHandler, MyAuthenticationFailureHandler failureHandler) {  
        this.successHandler = successHandler;  
        this.logoutSuccessHandler = logoutSuccessHandler;  
        this.failureHandler = failureHandler;  
    }  
  
    @Bean  
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {  
  

        http.formLogin(form -> form  
                .successHandler(successHandler) // 启用成功处理器  
                .failureHandler(failureHandler) // 启用失败处理器(
        );  
  
        http.logout(logout -> logout  
                .logoutSuccessHandler(logoutSuccessHandler) // 启用注销处理器
                .invalidateHttpSession(true) // 销毁 Session
        );  
  
        return http.build();  
    }  
  
}

先通过构造器注入handler,再在http构造时对应的lambda表达式处挂载。

跨域

跨域全称是跨域资源共享(Cross-Origin Resources Sharing,CORS),它是浏览器的保护机制,只允许网页请求统一域名下的服务,同一域名指=>协议、域名、端口号都要保持一致,如果有一项不同,那么就是跨域请求。在前后端分离的项目中,需要解决跨域的问题。

在SpringSecurity中解决跨域很简单,在配置文件中添加如下配置即可

//跨域
http.cors(withDefaults());

身份认证

基本概念

在这里插入图片描述

在Spring Security框架中,SecurityContextHolder、SecurityContext、Authentication、Principal和Credential是一些与身份验证和授权相关的重要概念。它们之间的关系如下:

  1. SecurityContextHolder:SecurityContextHolder 是 Spring Security 存储已认证用户详细信息的地方。
  2. SecurityContext:SecurityContext 是从 SecurityContextHolder 获取的内容,包含当前已认证用户的 Authentication 信息。
  3. Authentication:Authentication 表示用户的身份认证信息。它包含了用户的Principal、Credential和Authority信息。
  4. Principal:表示用户的身份标识。它通常是一个表示用户的实体对象,例如用户名。Principal可以通过Authentication对象的getPrincipal()方法获取。
  5. Credentials:表示用户的凭证信息,例如密码、证书或其他认证凭据。Credential可以通过Authentication对象的getCredentials()方法获取。
  6. GrantedAuthority:表示用户被授予的权限

总结起来,SecurityContextHolder用于管理当前线程的安全上下文,存储已认证用户的详细信息,其中包含了SecurityContext对象,该对象包含了Authentication对象,后者表示用户的身份验证信息,包括Principal(用户的身份标识)和Credential(用户的凭证信息)。

在Controller中获取用户信息

IndexController:

@RestController  
public class IndexController {  
  
    /**  
     * 获取当前登录用户的详细信息  
     * 用于学习 SecurityContextHolder 相关 API  
     */    @GetMapping("/user/info")  
    public String getUserInfo() {  
        // 1. 从 SecurityContextHolder 中获取认证对象  
        // SecurityContextHolder 是一个工具类,内部通过 ThreadLocal 存储了当前线程的认证信息  
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();  
  
        // 2. 获取用户身份主体 (在你的项目中,你自定义的 SecurityUser )  
        Object principal = authentication.getPrincipal();  
  
        // 3. 获取用户权限信息 (如角色 ROLE_ADMIN 等,均为自定义)  
        Object authorities = authentication.getAuthorities();  
  
        // 4. 获取认证状态 (是否已登录)  
        boolean isAuthenticated = authentication.isAuthenticated();  
  
        // 为了方便查看,我们将这些信息封装到 Map 中返回  
        Map<String, Object> info = new java.util.HashMap<>();  
        info.put("principal", principal);  
        info.put("authorities", authorities);  
        info.put("isAuthenticated", isAuthenticated);  
        info.put("name", authentication.getName()); // 获取用户名  
  
        // 使用 Fastjson2 手动转换为 JSON 字符串  
        return JSON.toJSONString(info);  
    }  
}

会话并发处理

简单来说就是,后登录的账号会使先登录的账号失效。

实现处理器接口SessionInformationExpiredStrategy

@Component  
public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {  
  
    @Override  
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {  
        // 1. 获取响应对象  
        HttpServletResponse response = event.getResponse();  
  
        // 2. 创建你要返回的 JSON 结果  
        Map<String, Object> result = new HashMap<>();  
        result.put("code", 401);  
        result.put("message", "您的账号已在其他地方登录,当前会话已失效");  
  
        // 3. 将结果转换为 JSON 并写入响应  
        response.setContentType("application/json;charset=UTF-8");  
        response.getWriter().println(JSON.toJSONString(result));  
    }  
}

config中挂载配置

http.sessionManagement(session -> session  
                .maximumSessions(1) // 限制同一个账号只能 1 个登录  
                .expiredSessionStrategy(sessionInformationExpiredStrategy) // 挂载自定义策略  
        );
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐