Skip to content

Spring Security

Spring Security 概述

Spring Security 是一个专注于身份认证和权限控制的安全框架。

Spring Security快速入门

1. 导入依赖

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>lndhdx-230601-springsecurity</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.7</version>
    </parent>

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

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

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

        <!-- junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. 权限配置

java
package com.futureweaver.conf;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(conf ->
            conf
                .requestMatchers("*.html", "*.js", "*.css")
                .permitAll()
        ).authorizeHttpRequests(conf ->
            conf
                .anyRequest()
                .authenticated()
        ).formLogin(conf ->
            conf.permitAll()
        );
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User
                .withUsername("user")
                .password("user")
                .roles("USER")
                .build();

        UserDetails admin = User
                .withUsername("admin")
                .password("admin")
                .roles("ADMIN", "USER")
                .build();

        return new InMemoryUserDetailsManager(user, admin);
    }
}

3. 编写资源

  • Application.java
java
package com.futureweaver.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan("com.futureweaver")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • UserController.java
java
package com.futureweaver.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.ArrayList;
import java.util.List;

@Controller
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/query")
    @ResponseBody
    public List<String> query() {
        List<String> result = new ArrayList<>();
        result.add("zhangsan");
        result.add("lisi");
        result.add("wangwu");

        return result;
    }
}
  • index.html

放到resource的static目录下

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1>hello world!</h1>

</body>
</html>

基于注解的权限控制

1. 更改配置

java
package com.futureweaver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(
        prePostEnabled = true,
        securedEnabled = true,
        jsr250Enabled = true
)
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.formLogin(conf ->
            conf.permitAll()
        ).csrf(conf ->
            conf.disable()
        );
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User
                .withUsername("user")
                .password("user")
                .roles("USER")
                .build();

        UserDetails admin = User
                .withUsername("admin")
                .password("admin")
                .roles("ADMIN", "USER")
                .build();

        return new InMemoryUserDetailsManager(user, admin);
    }
}

2. 添加权限注解

java
package com.futureweaver.web;

import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.ArrayList;
import java.util.List;

@Controller
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/query")
    @ResponseBody
    @Secured("ROLE_ADMIN")
    public List<String> query() {
        List<String> result = new ArrayList<>();
        result.add("zhangsan");
        result.add("lisi");
        result.add("wangwu");

        return result;
    }
}

基于注解的复杂权限控制

java
package com.futureweaver.web;

import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.ArrayList;
import java.util.List;

@Controller
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/add")
    @ResponseBody
    @PreAuthorize("isAuthenticated() and principal.username.equals('zhangsan') or hasAuthority('ROLE_ADMIN')")
    public void add() {
    }
}

基于RESTful风格的配置

待解决问题

  1. 出错时

    • 未登录时,提示要登录页面修改
    • 无权限时,提示错误信息[目前可行]
  2. 登录相关

    • 登录服务地址变更
    • 登录成功时的响应
    • 登录失败时的响应
    • 修改参数名username => realname; password => pwd
  3. 退出相关

    • 退出服务地址变更
    • 退出成功时的响应

解决

  • SecurityConfig.java
java
package com.futureweaver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(
        prePostEnabled = true,
        securedEnabled = true,
        jsr250Enabled = true
)
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.formLogin(conf ->
            conf
                .loginProcessingUrl("/admin/login")
                .successForwardUrl("/admin/login_success")
                .failureForwardUrl("/admin/login_failure")
                .usernameParameter("realname")
                .passwordParameter("pass")
                .permitAll()
        ).logout(conf ->
            conf
                .logoutUrl("/admin/logout")
                .logoutSuccessUrl("/admin/logout_success")
                .permitAll()
        ).exceptionHandling(conf ->
            // 401 Unauthorized 页面配置
            // 403 Forbidden 页面配置
            conf.authenticationEntryPoint(authenticationEntryPoint())
        ).authorizeHttpRequests(conf ->
            conf
                .anyRequest()
                .permitAll()
        ).csrf(conf ->
            conf.disable()
        );

        return http.build();
    }

    AuthenticationEntryPoint authenticationEntryPoint() {
        return new BasicAuthenticationEntryPoint();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails zhangsan = User
                .withUsername("zhangsan")
                .password("123456")
                .roles("USER")
                .build();

        UserDetails lisi = User
                .withUsername("lisi")
                .password("123456")
                .roles("USER")
                .build();

        UserDetails admin = User
                .withUsername("admin")
                .password("admin")
                .roles("ADMIN", "USER")
                .build();

        return new InMemoryUserDetailsManager(zhangsan, lisi, admin);
    }
}
  • AuthController
java
package com.futureweaver.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/admin")
public class AuthController {
    @RequestMapping("/login_success")
    @ResponseBody
    public String loginSuccess() {
        return "login success";
    }

    @RequestMapping("/login_failure")
    @ResponseBody
    public String loginFailure() {
        return "login failure";
    }

    @RequestMapping("/logout_success")
    @ResponseBody
    public String logoutSuccess() {
        return "logout success";
    }
}

自定义UserDetailsService

如果想要实际地查询数据库,将数据库返回的数据库作为登录的依据

就必须使用springsecurity中的UserDetailsService和UserDetails完成

1. 导入依赖

  • com.mysql:mysql-connector-j
  • com.alibaba:druid-spring-boot-starter:1.2.20
  • org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.2

2. 编写springboot配置文件

properties
# mybatis-settings
mybatis.configuration.lazy-loading-enabled=true
mybatis.configuration.auto-mapping-behavior=full
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.cache-enabled=false
mybatis.configuration.local-cache-scope=statement
# mybatis-typeAliases
mybatis.type-aliases-package=com.futureweaver.domain
# datasource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shopping?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

3. 给启动器添加MapperScan注解

java
package com.futureweaver.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan("com.futureweaver")
@MapperScan("com.futureweaver.mapper")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

4. 删除SecurityConfig中的UserDetailsService的Bean配置

java
package com.futureweaver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(
        prePostEnabled = true,
        securedEnabled = true,
        jsr250Enabled = true
)
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.formLogin(conf ->
            conf
                .loginProcessingUrl("/admin/login")
                .successForwardUrl("/admin/login_success")
                .failureForwardUrl("/admin/login_failure")
                .usernameParameter("realname")
                .passwordParameter("pass")
                .permitAll()
        ).logout(conf ->
            conf
                .logoutUrl("/admin/logout")
                .logoutSuccessUrl("/admin/logout_success")
                .permitAll()
        ).exceptionHandling(conf ->
            // 401 Unauthorized 页面配置
            // 403 Forbidden 页面配置
            conf.authenticationEntryPoint(authenticationEntryPoint())
        ).authorizeHttpRequests(conf ->
            conf
                .anyRequest()
                .permitAll()
        ).csrf(conf ->
            conf.disable()
        );

        return http.build();
    }

    AuthenticationEntryPoint authenticationEntryPoint() {
        return new BasicAuthenticationEntryPoint();
    }
}

5. 编写UserDetailsService的实现类

java
package com.futureweaver.service;

import com.futureweaver.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service("userDetailsService")
public class UserService implements UserDetailsService {
    @Autowired
    private UserMapper mapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return mapper.queryByUsername(username);
    }
}

6. 编写UserDetails的实现类

java
package com.futureweaver.domain;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

public class User implements UserDetails {
    // Apache Shiro
    // Spring Security
    private String id;
    private String username;
    private String password;
    private String lastLoginIp;
    private String lastLoginTime;
    private String avatar;
    private String roleIds;

    @Override
    public String toString() {
        return "User{" +
                "id='" + id + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", lastLoginIp='" + lastLoginIp + '\'' +
                ", lastLoginTime='" + lastLoginTime + '\'' +
                ", avatar='" + avatar + '\'' +
                ", roleIds='" + roleIds + '\'' +
                '}';
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getLastLoginIp() {
        return lastLoginIp;
    }

    public void setLastLoginIp(String lastLoginIp) {
        this.lastLoginIp = lastLoginIp;
    }

    public String getLastLoginTime() {
        return lastLoginTime;
    }

    public void setLastLoginTime(String lastLoginTime) {
        this.lastLoginTime = lastLoginTime;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    public String getRoleIds() {
        return roleIds;
    }

    public void setRoleIds(String roleIds) {
        this.roleIds = roleIds;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}