前言:看前了解

强烈建议看这个之前一定要把:Spring-- Spring Security(一)-CSDN博客这篇博客的知识点4看了 重点是这个解以及解析

  • 1. 了解 UserDetailsService

    • 本篇的 SpringDataUserDetailsService 就是继承的 UserDetailsService

  • 2. 了解 UserDetailsServiceloadUserByUsername 方法

    • 本篇的 SpringDataUserDetailsService 就是继承 UserDetailsService 并重写了 loadUserByUsername 方法,用于根据用户名查询用户信息。

  • 3. 了解 UserDetails 是啥

    • UserDetails 是由 UserDetailsServiceloadUserByUsername 方法产生的对象。

    • 本篇就是通过 SpringDataUserDetailsService(继承 UserDetailsService 并重写 loadUserByUsername 方法)生成 UserDetails 对象。

  • 4. 了解 AuthenticationManager 是什么

    • AuthenticationManager 是一个接口,表示认证的管理器,只有一个方法:

      Authentication authenticate(Authentication authentication) throws AuthenticationException;
    • 作用:接收一个待认证的 Authentication 对象,然后调用内部的 AuthenticationProvider 完成用户身份验证。

    • 认证成功:返回一个已认证的 Authentication 对象,其中包含用户信息和权限。

    • 认证失败:会抛出 AuthenticationException 异常,比如密码错误、用户不存在等。

    • 这也是为什么在自定义 Controller 的登录接口中,需要注入 AuthenticationManager

  • 5. 了解 Authentication 是做什么的

    • Authentication 是用户认证状态的封装对象,会存储在 SecurityContextHolder 中,供后续授权使用。

    • 在自定义 Controller 的登录接口中,Authentication 就是通过 AuthenticationManager.authenticate(authRequest) 得到的对象。

    • 它包含:

      • principal:用户身份(用户名或 UserDetails 对象)

      • credentials:凭证(密码,认证成功后可为空)

      • authorities:权限列表

      • authenticated:是否认证成功

一、Spring Security 简介

Spring Security 是 Spring 家族中的安全框架,主要提供:

  • 认证(Authentication):用户是谁(登录验证)。

  • 授权(Authorization):用户能做什么(访问权限控制)。

  • 防护(Protection):常见攻击防护,如 CSRF、会话劫持等。

在 Spring Boot 项目中,Spring Security 常见的使用方式是:

  • 配置 安全拦截规则(哪些 URL 需要登录、哪些需要权限)。

  • 定义 用户认证信息来源(内存 / 数据库 / 第三方服务)。

  • 配置 密码加密登录 / 登出页面


二、核心代码与配置

1. 视图配置(代替 SpringMVC.xml)

@Configuration // 相当于 springmvc.xml
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 访问根路径 “/” 时,重定向到 /login-view
        registry.addViewController("/").setViewName("redirect:/login-view");

        // 配置登录页映射:/login-view -> login.jsp
        registry.addViewController("/login-view").setViewName("login");
    }
}

2. Spring Security 核心配置(WebSecurityConfig 类)

旧写法(Spring Boot 2.6- 之前)
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
// 开启基于方法的权限控制(@PreAuthorize、@Secured)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

 //定义用户信息服务(查询用户信息),使用了SpringDataUserDetailsService类 就不用使用这个方法
/*
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
              manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }
*/

    //密码编码器(不对密码进行加密,密码是明文存储 & 明文校验。)【不推荐使用】
    /*@Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }*/
    // 密码编码器(推荐使用 BCrypt,避免明文存储)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 安全拦截配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable() // 关闭 CSRF(前后端分离常关闭,表单需开启)
                .authorizeRequests()
//               .antMatchers("/r/r1").hasAuthority("p1")//基于url的方式授权,但是一般使用基于方法的方式授权即一般在controller的方法上加@PreAuthorize("hasAuthority('p1')")
//              .antMatchers("/r/r2").hasAuthority("p2")//基于url的方式授权,但是一般使用基于方法的方式授权即一般在controller的方法上加@PreAuthorize("hasAuthority('p2')")
                .antMatchers("/r/**").authenticated() // 所有 /r/** 需要认证
                .anyRequest().permitAll() // 其他请求直接放行(这个一定要放在最后,因为他是从上到下,放一个符合直接放行了  要把细的规则放上面,范围更大的规则放下面)
                .and()
                .formLogin() // 开启表单登录
                .loginPage("/login-view") //用户没有登录的话要跳转到 /login-view 这个路径 ,如果不指定,Spring Security会生成一个默认的login登录页面
                .loginProcessingUrl("/login") // 登录表单提交地址【与jsp中定义的<form action="login" method="post">的action要相同】
                .successForwardUrl("/login-success") // 登录成功跳转
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 按需创建 session
                .and()
                .logout()
                .logoutUrl("/logout") // 登出请求
                .logoutSuccessUrl("/login-view?logout"); // 登出成功后跳转
    }
}
新写法(Spring Security 5.7+ 推荐)
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig {

    // 密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 核心:HttpSecurity 配置
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/**").authenticated()
                .anyRequest().permitAll()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login-view?logout");

        // 注意:如果你走自定义 JSON 登录接口,就不要开启 formLogin()
        return http.build();
    }
}

3. 登录 & 资源访问控制器

@RestController
public class LoginController {

    // 登录成功后跳转到这里
    @RequestMapping(value = "/login-success", produces = {"text/plain;charset=UTF-8"})
    public String loginSuccess() {
        return getUsername() + " 登录成功";
    }

    // 资源1:需要 p1 权限
    @GetMapping(value = "/r/r1", produces = {"text/plain;charset=UTF-8"})
    @PreAuthorize("hasAuthority('p1')")
    public String r1() {
        return getUsername() + " 访问资源1";
    }

    // 资源2:需要 p2 权限
    @GetMapping(value = "/r/r2", produces = {"text/plain;charset=UTF-8"})
    @PreAuthorize("hasAuthority('p2')")
    public String r2() {
        return getUsername() + " 访问资源2";
    }

    // 获取当前登录用户
    private String getUsername() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        Object principal = authentication.getPrincipal();
        if (principal instanceof UserDetails) {
            return ((UserDetails) principal).getUsername();
        }
        return principal.toString();
    }
}

4. 数据库访问层

@Repository
public class UserDao {

    @Autowired
    JdbcTemplate jdbcTemplate;

    // 根据账号查询用户信息
    public UserDto getUserByUsername(String username) {
        String sql = "select id,username,password,fullname,mobile from t_user where username = ?";
        List<UserDto> list = jdbcTemplate.query(sql,
                new Object[]{username},
                new BeanPropertyRowMapper<>(UserDto.class));
        return (list != null && list.size() == 1) ? list.get(0) : null;
    }

    // 根据用户id查询权限
    public List<String> findPermissionsByUserId(String userId) {
        String sql = "SELECT * FROM t_permission WHERE id IN(" +
                "SELECT permission_id FROM t_role_permission WHERE role_id IN(" +
                "  SELECT role_id FROM t_user_role WHERE user_id = ? ))";

        List<PermissionDto> list = jdbcTemplate.query(sql,
                new Object[]{userId},
                new BeanPropertyRowMapper<>(PermissionDto.class));

        List<String> permissions = new ArrayList<>();
        list.forEach(c -> permissions.add(c.getCode()));
        return permissions;
    }
}

5. 用户与权限 DTO

@Data
public class UserDto {
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
}

@Data
public class PermissionDto {
    private String id;
    private String code;        // 权限标识
    private String description; // 描述
    private String url;         // 资源路径
}

6. 自定义 UserDetailsService(认证逻辑)

//这个就是4.2 认证(Authentication)流程中的图解的第五步
//我们实现UserDetailsService接口,重写了loadUserByUsername方法,返回我们定义的UserDetails 
@Service
public class SpringDataUserDetailsService implements UserDetailsService {

    @Autowired
    UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        UserDto userDto = userDao.getUserByUsername(username);
        if (userDto == null) {
            throw new UsernameNotFoundException("用户不存在");
        }

        // 查询用户权限
        List<String> permissions = userDao.findPermissionsByUserId(userDto.getId());
        String[] permissionArray = permissions.toArray(new String[0]);

        // 构造 Spring Security User
        return User.withUsername(userDto.getUsername())
                .password(userDto.getPassword())
                .authorities(permissionArray)
                .build();
    }
}

7. 启动类

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

8. 配置文件(application.properties)

server.port=8080
server.servlet.context-path=/security-springboot
spring.application.name=security-springboot

spring.mvc.view.prefix=/WEB-INF/view/
spring.mvc.view.suffix=.jsp

spring.datasource.url=jdbc:mysql://localhost:3306/user_db
spring.datasource.username=root
spring.datasource.password=mysql
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

9. 登录页面(login.jsp)

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %>
<html>
<head>
    <title>用户登录</title>
</head>
<body>
<form action="login" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="password"><br>
    <input type="submit" value="登录">
</form>
</body>
</html>

10. Maven 依赖

<dependencies>
    <!-- Web 依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Security 依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- JDBC + MySQL -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

    <!-- JSP 视图支持 -->
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
    </dependency>
</dependencies>

三、总结

  1. 登录认证:由 Spring Security 接管,使用 UserDetailsService 从数据库加载用户信息。

  2. 权限控制:支持 URL 拦截(antMatchers)和方法级拦截(@PreAuthorize)。

  3. 密码加密:使用 BCryptPasswordEncoder,避免明文存储。

  4. 前后端交互:表单方式(JSP)提交到 /login,由 Security 自动处理。

四、如果想自己定义controller的登录接口应该如何做?

1.WebSecurityConfig关闭configure方法的登录(也就是注释这个

//.formLogin()//允许表单登录
//.loginPage("/login-view")//登录页面
//.loginProcessingUrl("/login")
//.successForwardUrl("/login-success")//自定义登录成功的页面地址

2.自定义登录接口(在controller中添加/Login接口【注意与html中from表单的提交地址保持一致】)

@RestController
public class LoginController {
    @Autowired
    private AuthenticationManager authenticationManager;

    @PostMapping("/doLogin")
    public String login(@RequestParam String username, @RequestParam String password) {
        try {
        // 封装认证请求
        UsernamePasswordAuthenticationToken authRequest =
                new UsernamePasswordAuthenticationToken(username, password);

        // 调用 Security 认证流程
        Authentication authentication = authenticationManager.authenticate(authRequest);

        // 认证成功
        SecurityContextHolder.getContext().setAuthentication(authentication);
        return username + " 登录成功(自定义接口)";
        } catch (Exception e) {
            e.printStackTrace();
            return "登录失败";
        }
    }

注意点

    1.要注入AuthenticationManager类 

    2.在本样例在输入错误密码的时候会报异常,要做异常处理,如果在controller 里没有捕获异常,导致前一次抛出的异常被返回给前端,Session 状态可能也被影响。【甚至可能会影响到后续如果输入正确也不可以了(不做登出的情况)】

3. 使用AuthenticationManager 的时候要在WebSecurityConfig里面把这个类设置成bean

1. 旧写法(Spring Boot 2.6- 之前)

在上面的的二的2.中添加

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
2. 新写法(Spring Security 5.7+ 推荐)

在上面的的二的2.中添加

// 配置 AuthenticationManager(需要时手动暴露)
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

五、如果想自己定义controller的登出接口应该如何做?

1.实现contrller层的代码

@GetMapping("/logout")
public String logout(HttpServletRequest request) {
    // 1. 清空认证信息
    SecurityContextHolder.clearContext();
    // 2. 使 Session 失效
    request.getSession().invalidate();
    return "退出成功";
}

2.不使用Spring Security自带的(也就是把WebSecurityConfig 类的登出注释)

//.logout()
//.logoutUrl("/logout")
//.logoutSuccessUrl("/login-view?logout");

Logo

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

更多推荐