SpringSecurity

  • 一、SpringSecurity基本介绍
  • 二、SpringBoot整合SpringSecurity
  • 2.1 添加SpringSecurity的依赖
  • 2.2 启动访问
  • 三、使用自定义登录界面
  • 3.1 创建Security配置类
  • 3.2 创建登录页面login.html
  • 3.3 启动访问
  • 四、使用数据库认证
  • 4.1 创建一个Service继承UserDetailService
  • 4.2 根据username获取用户信息
  • 4.3 修改 SecurityConfig
  • 4.4 重启访问
  • 五、密码加密认证
  • 5.1 修改UserDetailsService实现类
  • 5.2 修改 SecurityConfig
  • 5.3 重启访问
  • 六、授权
  • 6.1 开启全局方法配置
  • 6.2 修改 UserDetailsService
  • 6.3 接口添加声明书注解
  • 6.3.1 启用prePostEnabled
  • 6.3.1.1 PreAuthorize、PostAuthorize
  • 6.3.1.2 PreFilter、PostFilter
  • 6.3.1.3 prePostEnabled的内置表达式
  • 6.3.2 启用securedEnabled
  • 6.3.3 启用jsr250Enabled
  • 6.4 重启访问
  • 七、账号验证异常处理


一、SpringSecurity基本介绍

Spring Security 是一个基于 Spring 的企业应用系统提供声明式安全访问控制接口方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的Bean,充分利用了Spring核心组件 IoC,DI 和 AOP ,为应用系统提供声明书的安全访问控制功能,减少了企业系统安全控制大量重复代码的编写。

二、SpringBoot整合SpringSecurity

2.1 添加SpringSecurity的依赖

新建一个SpringBoot项目,添加如下依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--thymeleaf的依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--SpringSecurity的依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2.2 启动访问

重启访问即可跳转到对应的登录界面

springboot 防范 xss PDF springboot springsecurity_java


注意:系统启动的时候会自动创建一个账号是 user,密码随机的用户(密码显示在控制台)

springboot 防范 xss PDF springboot springsecurity_spring boot_02

使用该用户即可登录成功

三、使用自定义登录界面

3.1 创建Security配置类

SecurityConfig

@Configuration
// 开启Security配置
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 认证配置
        auth.inMemoryAuthentication()
                .withUser("test01") // 登录账号
                .password("{noop}123") // 登录密码,其中{noop}表示不对密码进行加密验证
                .roles("USER_ROLE"); // 账户角色
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // Http请求配置
        http.formLogin()
                .loginPage("/login.html") // 指定自定义的登录界面
                .loginProcessingUrl("/login.do") // 注意:必须和登录表单的 action一致!!!
                .and()
                .authorizeRequests() // 定义哪些资源被保护
                .antMatchers("/login.html")
                .permitAll() // login.html可以匿名访问
                .anyRequest()
                .authenticated(); // 除登录页面其他都需要认证
        http.csrf().disable(); // 禁用跨域攻击
    }
}

3.2 创建登录页面login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title></head>
<body>
<h2>自定义登录页面</h2>
<form action="/login.do" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

注意:登录页面需放入resources的status目标下,如图:

3.3 启动访问

重启访问即可跳转到自定义的登录界面

springboot 防范 xss PDF springboot springsecurity_html_03

四、使用数据库认证

4.1 创建一个Service继承UserDetailService

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
public class UserService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 重写 UserDetailsService 中的 loadUserByUsername 方法
        return null;
    }
}

4.2 根据username获取用户信息

根据username获取用户基本信息,角色信息等

@Service
public class UserService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 重写 UserDetailsService 中的 loadUserByUsername 方法
        // 用户不存在直接返回 null:表示无权访问
        /**
         * 模拟数据库操作 根据username获取用户基本信息,角色信息等
         */
        String password = "123";

        // 用户角色
        List<SimpleGrantedAuthority> list = new ArrayList<>();
        list.add(new SimpleGrantedAuthority("USER_ROLE"));

        // {noop}表示不对密码进行加密验证
        UserDetails userDetails = new User(username, "{noop}" + password, list);
        return userDetails;
    }
}

4.3 修改 SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 认证配置
        /*auth.inMemoryAuthentication()
                .withUser("test01")
                .password("{noop}123")
                .roles("USER_ROLE");*/
        // 添加自定义登录验证
        auth.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // Http请求配置
        http.formLogin()
                .loginPage("/login.html") // 指定自定义的登录界面
                .loginProcessingUrl("/login.do") // 必须和登录表单的 action一致
                .and()
                .authorizeRequests() // 定义哪些资源被保护
                .antMatchers("/login.html")
                .permitAll() // login.html可以匿名访问
                .anyRequest()
                .authenticated(); // 除登录页面其他都需要认证
        http.csrf().disable(); // 禁用跨域攻击
    }
}

4.4 重启访问

springboot 防范 xss PDF springboot springsecurity_spring_04

五、密码加密认证

5.1 修改UserDetailsService实现类

@Service
public class UserService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 重写 UserDetailsService 中的 loadUserByUsername 方法
        // 用户不存在直接返回 null:表示无权访问
        /**
         * 模拟数据库操作 根据username获取用户基本信息,角色信息等
         */
        // 加密后的密码
        String password = "$2a$10$XN/6HZGG1M1TfOr4Hj4hHeSh48BLHQom7g8aveHEm8MvCAyW6ajN2";

        // 用户角色
        List<SimpleGrantedAuthority> list = new ArrayList<>();
        list.add(new SimpleGrantedAuthority("USER_ROLE"));

        // 去除 {noop},表示需要对密码进行加密验证
        // UserDetails userDetails = new User(username, "{noop}" + password, list);
        UserDetails userDetails = new User(username, password, list);
        return userDetails;
    }

	// 密码生成器
    public static void main(String[] args) {
        // SHA-256 + 随机salt
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123");
        System.out.println(password);
    }
}

5.2 修改 SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 认证配置
        /*auth.inMemoryAuthentication()
                .withUser("test01")
                .password("{noop}123")
                .roles("USER_ROLE");*/
        // 添加自定义登录验证
        auth.userDetailsService(userService)
        		// 指定密码 解密器
                .passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // Http请求配置
        http.formLogin()
                .loginPage("/login.html") // 指定自定义的登录界面
                .loginProcessingUrl("/login.do") // 必须和登录表单的 action一致
                .and()
                .authorizeRequests() // 定义哪些资源被保护
                .antMatchers("/login.html")
                .permitAll() // login.html可以匿名访问
                .anyRequest()
                .authenticated(); // 除登录页面其他都需要认证
        http.csrf().disable(); // 禁用跨域攻击
    }
}

5.3 重启访问

springboot 防范 xss PDF springboot springsecurity_spring_04

六、授权

6.1 开启全局方法配置

SecurityConfig 类上添加 @EnableGlobalMethodSecurity 注解。

@Configuration
@EnableWebSecurity
// 开启全局方法配置:这个注解必须开启,否则@PreAuthorize等注解不生效
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

其中注解 @EnableGlobalMethodSecurity 有几个方法:

  • prePostEnabled: 确定前置注解[@PreAuthorize,@PostAuthorize,@PreFilter,@PostFilter] 是否启用
  • securedEnabled: 确定安全注解 [@Secured] 是否启用
  • jsr250Enabled: 确定 JSR-250注解 [@RolesAllowed…]是否启用

在同一个应用程序中,可以启用多个类型的注解,但是只应该设置一个注解对于行为类的接口或者类。

6.2 修改 UserDetailsService

为用户添加角色

@Service
public class UserService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 重写 UserDetailsService 中的 loadUserByUsername 方法
        // 用户不存在直接返回 null:表示无权访问
        /**
         * 模拟数据库操作 根据username获取用户基本信息,角色信息等
         */
        // 加密后的密码
        String password = "$2a$10$XN/6HZGG1M1TfOr4Hj4hHeSh48BLHQom7g8aveHEm8MvCAyW6ajN2";

        // 用户角色
        List<SimpleGrantedAuthority> list = new ArrayList<>();
        if ("test01".equals(username)) {
            // 如果用户为 test01 为其添加 USER_ROLE 角色
            list.add(new SimpleGrantedAuthority("USER_ROLE"));
        }

        // 去除 {noop},表示需要对密码进行加密验证
        // UserDetails userDetails = new User(username, "{noop}" + password, list);
        UserDetails userDetails = new User(username, password, list);
        return userDetails;
    }

    public static void main(String[] args) {
        // SHA-256 + 随机salt
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123");
        System.out.println(password);
    }
}

6.3 接口添加声明书注解

6.3.1 启用prePostEnabled

Spring Security 中定义了四个支持使用表达式的注解:

  • @PreAuthorize
  • @PostAuthorize
  • @PreFilter
  • @PostFilter

其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。

注意:要使它们的定义能够对我们的方法的调用产生影响,在 Spring 中,我们需要设置
global-method-security元素的pre-post-annotations=”enabled”,默认为disabled。

<security:global-method-security pre-post-annotations="disabled"/>

在springboot中,需要添加 @EnableGlobalMethodSecurity(prePostEnabled = true) 注解。

6.3.1.1 PreAuthorize、PostAuthorize
  • @PreAuthorize 是在方法执行前进行权限认证
  • @PostAuthorize方法执行后在返回前进行权限认证

示例语法:

// 拥有 ROLE_USER 才能访问
@PreAuthorize("hasRole('ROLE_USER')")

// 拥有 ROLE_USER 或 ROLE_ADMIN 才能访问
@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")

// 拥有 sys:dept:delete 中的一种角色才能访问
@PreAuthorize("hasAuthority('sys:dept:delete')")

// 限制只能 Id小于10的用户才能访问
@PreAuthorize("#id<10")

// 限制只有username参数为自己用户名才能访问
@PreAuthorize("principal.username.equals(#username)")
public User find(String username) {
}

// 限制只有用户名称为abc的用户才能访问
@PreAuthorize("#user.name.equals('abc')")
public void add(User user) {
}
6.3.1.2 PreFilter、PostFilter
  • @PreFilter 是在执行方法之前过滤集合或数组
  • @PostFilter 是在执行方法后过滤返回的集合或数组

@PreFilter 和 @PostFilter 可以对集合类型的参数或返回值进行过滤。使用 @PreFilter 和 @PostFilter 时,Spring Security 将移除使对应表达式的结果为 false 的元素。

示例语法:

// 移除返回结果中id不为偶数的user
@PostFilter("filterObject.id%2==0")
public List<User> findAll() {
}

// 移除ids集合中不为偶数的对象
@PreFilter(filterTarget = "ids", value = "filterObject%2==0")
public void delete(List<Integer> ids, List<String> usernames) {
}

注意:filterObject 是使用 @PreFilter 和 @PostFilter 时的一个内置表达式,表示集合中的当前对象。当 @PreFilter 标注的方法拥有多个集合类型的参数时,需要通过 @PreFilter 的filterTarget 属性指定当前 @PreFilter 是针对哪个参数进行过滤的。

6.3.1.3 prePostEnabled的内置表达式

表达式

描述

hasRole([role])

当前用户是否拥有指定角色。

hasAnyRole([role1,role2])

多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。

hasAuthority([auth])

等同于hasRole

hasAnyAuthority([auth1,auth2])

等同于hasAnyRole

Principle

代表当前用户的principle对象

authentication

直接从SecurityContext获取的当前Authentication对象

permitAll

总是返回true,表示允许所有的

denyAll

总是返回false,表示拒绝所有的

isAnonymous()

当前用户是否是一个匿名用户

isRememberMe()

表示当前用户是否是通过Remember-Me自动登录的

isAuthenticated()

表示当前用户是否已经登录认证成功了。

isFullyAuthenticated()

如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录的,则返回true。

6.3.2 启用securedEnabled

在 Springboot 中,需要添加 @EnableGlobalMethodSecurity(securedEnabled = true)) 注解。

在需要控制访问的接口上,添加 @Secured 注解。

@RestController
public class TestController {

    /**
     * 只有拥有 USER_ROLE 角色的用户才能访问
     * @return
     */
    @Secured({"USER_ROLE"})
    @GetMapping("/test")
    public String test() {
        return "success";
    }

    /**
     * 不添加任何授权注解:表示所有用户均可访问
     * @return
     */
    @GetMapping("/test1")
    public String test1() {
        return "success";
    }
}

@Secured 注解是用来定义业务方法的安全配置。在需要安全[角色/权限等]的方法上指定 @Secured,并且只有那些角色/权限的用户才可以调用该方法。

@Secured 缺点(限制)就是不支持 Spring EL 表达式。不够灵活。并且指定的角色必须以 ROLE_ 开头,不可省略

6.3.3 启用jsr250Enabled

在 Springboot 中,需要添加 @EnableGlobalMethodSecurity(jsr250Enabled = true) 注解。

jsr250Enabled 注解比较简单,只有

  • @DenyAll: 拒绝所有访问
  • @RolesAllowed({“USER”, “ADMIN”}): 该方法只要具有"USER", "ADMIN"任意一种权限就可以访问。这里可以省略前缀ROLE_,实际的权限可能是ROLE_ADMIN
  • @PermitAll: 允许所有访问

6.4 重启访问

使用不同账号登录访问,只有用户名为 test01 的账号存在 /test 接口访问权限。

springboot 防范 xss PDF springboot springsecurity_spring_04

七、账号验证异常处理

在实际开发中,对于登录异常,往往需要给出友好的错误提示,如:账户被锁定,密码过期,用户被禁用等,SpringSecurity 给我们一共了友好的身份校验失败异常处理器,AuthenticationFailureHandler,如下:

@Configuration
@EnableWebSecurity
// 开启全局方法配置:这个注解必须开启,否则@PreAuthorize等注解不生效
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 认证配置
        /*auth.inMemoryAuthentication()
                .withUser("test01")
                .password("{noop}123")
                .roles("USER_ROLE");*/
        // 添加自定义登录验证
        auth.userDetailsService(userService)
                .passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // Http请求配置
        http.formLogin()
                .loginPage("/login.html") // 指定自定义的登录界面
                .loginProcessingUrl("/login.do") // 必须和登录表单的 action一致
                // 身份验证失败 异常处理器
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=UTF-8");
                        httpServletResponse.setStatus(403);
                        Map<String, Object> errorMsgMap = new HashMap<>(2);
                        errorMsgMap.put("code", 403);
                        if (e instanceof BadCredentialsException ||
                                e instanceof UsernameNotFoundException) {
                            errorMsgMap.put("message", "账户名或者密码输入错误!");
                        } else if (e instanceof LockedException) {
                            errorMsgMap.put("message", "账户被锁定,请联系管理员!");
                        } else if (e instanceof CredentialsExpiredException) {
                            errorMsgMap.put("message", "密码过期,请联系管理员!");
                        } else if (e instanceof AccountExpiredException) {
                            errorMsgMap.put("message", "账户过期,请联系管理员!");
                        } else if (e instanceof DisabledException) {
                            errorMsgMap.put("message", "账户被禁用,请联系管理员!");
                        } else {
                            errorMsgMap.put("message", "登录失败!");
                        }
                        httpServletResponse.getWriter().write(errorMsgMap.toString());
                    }
                })
                .and()
                .authorizeRequests() // 定义哪些资源被保护
                .antMatchers("/login.html")
                .permitAll() // login.html可以匿名访问
                .anyRequest()
                .authenticated(); // 除登录页面其他都需要认证
        http.csrf().disable(); // 禁用跨域攻击
    }
}