1、认识 Spring Security

1.1、Spring Security 的核心功能

  • 认证 (你是谁)
  • 授权 (你能干什么)
  • 攻击防护 (防止伪造身份)

其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式。

1.2、Spring Security 的核心概念

  • Principle:代表用户的对象 Principle(User),不仅指人类,还包含一切可以用于验证的设备。
  • Authority:代表用户的角色 Authority(Role),每个用户都应该有一种角色,如管理员或是会员。
  • Permission:代表授权,复杂的应用环境需要对角色的权限进行描述。

在 Spring Security 中,Authority 和 Permission 是两个完全独立的概念,两者并没有必然的联系。他们之间需要通过配置进行关联,可以是自己定义的各种关系。

1.2.1、认证和授权

安全主要分为验证(authentication)和 授权(authorization)两个部分。

1.2.1.1、验证(authentication)

验证指的是,建立系统使用者(Principal)的过程。使用者可以是一个用户、设备,和可以在应用程序中执行某种操作的其他系统。

用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码的正确性来完成认证的通过或拒绝过程。

Spring Security 支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要验证、OpenID 和 LDAP 等

Spring Security 进行验证步骤:

  1. 用户使用用户名、密码登录
  2. 过滤器(UsernamePasswordAuthenticationFilter)获取到用户名、密码。然后封装成 Authentication
  3. AuthenticationManager 认证 token(Authentication 的实现类传递)
  4. AuthenticationManager 认证成功,返回一个封装了用户权限信息的 Authentication 对象,用户的上下文信息(角色列表等)
  5. Authentication 对象赋值给当前的 SecurityContext,建立这个用户的安全上下文(通过调用 SecurityContextHolder.getContext().setAuthentication())
  6. 用户进行一些受到访问控制机制保护的操作,访问控制机制会依据当前安全上下文信息检查这个操作需要的权限

除利用提供的认证外,还可以编写自己的 Filter(过滤器),提供与那些不是基于 Spring Security 的验证系统的操作。

1.2.1.2、授权(authorization)

在一个系统中,不同用户具有的权限是不同的。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

它判断某个 Principal 在应用程序中是否允许执行某个操作。在进行授权判断之前,要求其所要使用到的规则必须在验证过程中已经建立好了。

对 Web 资源的保护,最好的办法是使用过滤器。对方法调用的保护,最好的办法是使用 AOP。

Spring Security 在进行用户认证及授权时,也是通过各种 拦截器 和 APO 来控制权限访问的,从而实现安全。

2、引入

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

3、演示简单案例

3.1、简单引入

package com.example.controller;

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

@RestController
@RequestMapping("/security")
public class SecurityController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello Spring Security ~";
    }
}

3.2、访问

http://localhost:8080/security/hello

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4、设置用户密码

4.1、方法一:配置文件

application.properties

# Spring Security
spring.security.user.name=root
spring.security.user.password=123456

4.2、方法二:配置类

package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    // 必须要添加这个,否则会报错提示 PasswordEncoder 为 null
    @Bean
    PasswordEncoder passWord() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 密码加密
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123");
        // 配置用户名、密码、权限
        auth.inMemoryAuthentication().withUser("Lucy").password(password).roles("admin");
    }
}
  • 如果不添加@Bean,报错如下:
2021-05-30 11:17:31.080 ERROR 36832 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

4.3、方法三:自定义编写实现类

4.3.1、编写配置类

package com.example.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SelfSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsService userDetailsService;

    @Bean
    PasswordEncoder passWord() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passWord());
    }
}

4.3.2、编写用户名密码获取类

package com.example.service;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MyUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) 
        throws UsernameNotFoundException {
        // 设置权限,这里随便写了一个 role
        List<GrantedAuthority> auths = 
            AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        return new User("Mary", new BCryptPasswordEncoder().encode("123"), auths);
    }
}

4.4、查询数据库完成认证

import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserModel UM;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserPO user = UM.findByName(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        return new User(user.getName(), new BCryptPasswordEncoder().encode(user.getPswd()), auths);
    }
}

package com.example.model;

import com.example.jpa.UserPO;
import com.example.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class UserModel {

    @Autowired
    private UserRepository UR;

    public UserPO findByName(String name) {
        List<UserPO> userList = UR.findAllByName(name);
        System.out.println(userList);
        if (userList.size() == 0) {
            return null;
        }
        return userList.get(0);
    }
}

package com.example.repository;

import com.example.jpa.UserPO;
import org.springframework.data.repository.Repository;

import java.util.List;

//@Resource
public interface UserRepository extends Repository<UserPO, Long> {

    List<UserPO> findAllByName(String name);
}

package com.example.jpa;

import lombok.Data;

import javax.persistence.*;

@Data
@Entity
@Table(name = "user")
public class UserPO {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String pswd;
}

在这里插入图片描述

5、自定义用户登录页面

5.1、编写配置类

package com.example.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SelfSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsService userDetailsService;

    @Bean
    PasswordEncoder passWord() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passWord());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义自己编写的登录页面
        http.formLogin()
            // 登录页面设置
            .loginPage("/login.html")
            // 登录访问路径
            .loginProcessingUrl("/user/login")
            // 登录成功之后,跳转路径
            .defaultSuccessUrl("/security/index").permitAll()
            // 哪些页面需要登录,哪些不需要登录
            .and().authorizeRequests()
                // 这些访问不需要登录
                .antMatchers("/", "/security/hello", "/user/login").permitAll()
                // 所有请求都需要认证
                .anyRequest().authenticated()
            // 关闭CSRF防护
            .and().csrf().disable();
    }
}

5.2、编写登录页面

src/main/resources/static/login.html

  • 注意:这里提交的的表单必须是POST提交的“username”和“password”,否则Spring Security不能自动识别(可以查看原码)
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>登录</title>
    </head>
    <body>
        <form action="/user/login" method="post">
            用户名:<input type="text" name="username">
            <br>
            密码:<input type="text" name="password">
            <br>
            <input type="submit" value="login">
        </form>
    </body>
</html>

5.3、编写登录成功后页面

package com.example.controller;

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

@RestController
@RequestMapping("/security")
public class SecurityController {

    @GetMapping("/hello") // 按上面设置,这个地址可以直接访问
    public String hello() {
        return "Hello Spring Security ~";
    }

    @GetMapping("/index") // 按上面设置,这个地址访问先登录,成功后显示
    public String index() {
        return "登录成功 ~";
    }
}

6、基于角色或权限进行访问控制

  • 没有访问权限返回码:403

6.1、方法一:hasAuthority(单一权限设置)

基于 SelfSecurityConfig.java 修改

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 自定义自己编写的登录页面
    http.formLogin()
        // 登录页面设置
        .loginPage("/login.html")
        // 登录访问路径
        .loginProcessingUrl("/user/login")
        // 登录成功之后,跳转路径
        .defaultSuccessUrl("/security/index").permitAll()
        // 哪些页面需要登录,哪些不需要登录
        .and().authorizeRequests()
            // 这些访问不需要登录
            .antMatchers("/", "/security/hello", "/user/login").permitAll()
            // 当前用户具有admins权限才能访问
            .antMatchers("/security/admins").hasAuthority("admins")
            // 所有请求都需要认证
            .anyRequest().authenticated()
        // 关闭CSRF防护
        .and().csrf().disable();
}

基于 MyUserDetailsService.java 修改

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    UserPO user = UM.findByName(username);
    if (user == null) {
        throw new UsernameNotFoundException("用户名不存在");
    }
    // 用户权限
    List<GrantedAuthority> auths = 
        AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
    return new User(user.getName(), // 用户名
                    new BCryptPasswordEncoder().encode(user.getPswd()), // 用户密码
                    auths); // 用户权限
}

6.2、方法二:hasAnyAuthority(多权限设置)

基于 SelfSecurityConfig.java 修改

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 自定义自己编写的登录页面
    http.formLogin()
        // 登录页面设置
        .loginPage("/login.html")
        // 登录访问路径
        .loginProcessingUrl("/user/login")
        // 登录成功之后,跳转路径
        .defaultSuccessUrl("/security/index").permitAll()
        // 哪些页面需要登录,哪些不需要登录
        .and().authorizeRequests()
            // 这些访问不需要登录
            .antMatchers("/", "/security/hello", "/user/login").permitAll()
            // 当前用户具有 admins 或 manager 权限才能访问
         // .antMatchers("/security/admins").hasAuthority("admins")
            .antMatchers("/security/admins").hasAnyAuthority("admins", "manager")
            // 所有请求都需要认证
            .anyRequest().authenticated()
        // 关闭CSRF防护
        .and().csrf().disable();
}

6.3、方法三:hasRole(与前两种不太一样)

基于 SelfSecurityConfig.java 修改

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 自定义自己编写的登录页面
    http.formLogin()
        // 登录页面设置
        .loginPage("/login.html")
        // 登录访问路径
        .loginProcessingUrl("/user/login")
        // 登录成功之后,跳转路径
        .defaultSuccessUrl("/security/index").permitAll()
        // 哪些页面需要登录,哪些不需要登录
        .and().authorizeRequests()
            // 这些访问不需要登录
            .antMatchers("/", "/security/hello", "/user/login").permitAll()
            // 当前用户具有 admins 或 manager 权限才能访问
         // .antMatchers("/security/admins").hasAuthority("admins")
         // .antMatchers("/security/admins").hasAnyAuthority("admins", "manager")
            .antMatchers("/security/admins").hasRole("sale")
            // 所有请求都需要认证
            .anyRequest().authenticated()
        // 关闭CSRF防护
        .and().csrf().disable();
}

差异看原码:ExpressionUrlAuthorizationConfigurer.java

...
public ExpressionInterceptUrlRegistry hasRole(String role) {
    return access(ExpressionUrlAuthorizationConfigurer.hasRole(role));
}

public ExpressionInterceptUrlRegistry hasAuthority(String authority) {
    return access(ExpressionUrlAuthorizationConfigurer.hasAuthority(authority));
}

public ExpressionInterceptUrlRegistry hasAnyAuthority(String... authorities) {
    return access(ExpressionUrlAuthorizationConfigurer.hasAnyAuthority(authorities));
}
...
private static String hasRole(String role) {
    Assert.notNull(role, "role cannot be null");
    Assert.isTrue(!role.startsWith("ROLE_"),
            () -> "role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
    return "hasRole('ROLE_" + role + "')";
}

private static String hasAuthority(String authority) {
    return "hasAuthority('" + authority + "')";
}

private static String hasAnyAuthority(String... authorities) {
    String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','");
    return "hasAnyAuthority('" + anyAuthorities + "')";
}
...

基于 MyUserDetailsService.java 修改

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    UserPO user = UM.findByName(username);
    if (user == null) {
        throw new UsernameNotFoundException("用户名不存在");
    }
    // 用户权限
    List<GrantedAuthority> auths = 
        AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");
    return new User(user.getName(), // 用户名
                    new BCryptPasswordEncoder().encode(user.getPswd()), // 用户密码
                    auths); // 用户权限
}

6.4、方法四:hasAnyRole(与上面相同)

略。。。

7、自定义无权限403页面

在这里插入图片描述

7.1、编写配置类

基于 SelfSecurityConfig.java 修改

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 自定义 403 页面
    http.exceptionHandling().accessDeniedPage("/unAuth.html");
    // 自定义自己编写的登录页面
    http.formLogin()
        // 登录页面设置
        .loginPage("/login.html")
        // 登录访问路径
        .loginProcessingUrl("/user/login")
        // 登录成功之后,跳转路径
        .defaultSuccessUrl("/security/index").permitAll()
        // 哪些页面需要登录,哪些不需要登录
        .and().authorizeRequests()
            // 这些访问不需要登录
            .antMatchers("/", "/security/hello", "/user/login").permitAll()
            // 当前用户具有 admins 或 manager 权限才能访问
         // .antMatchers("/security/admins").hasAuthority("admins")
         // .antMatchers("/security/admins").hasAnyAuthority("admins", "manager")
            .antMatchers("/security/admins").hasRole("sale")
            // 所有请求都需要认证
            .anyRequest().authenticated()
        // 关闭CSRF防护
        .and().csrf().disable();
}

7.2、编写 403 页面

src/main/resources/static/unAuth.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>无权限</title>
    </head>
    <body>
      <h1>没有访问权限</h1>
    </body>
</html>
Logo

Authing 是一款以开发者为中心的全场景身份云产品,集成了所有主流身份认证协议,为企业和开发者提供完善安全的用户认证和访问管理服务

更多推荐