Spring Boot整合Spring Security

白星腾
2023-12-01

Spring Boot整合Spring Security

Spring Security的安全管理两个重要概念是:认证(Authentication)和授权(Authorization)。认证即对用户登录进行管控,授权即对登录用户的权限进行管控。

一、配置

添加spring-boot-starter-security启动器,pom中引入依赖

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

一旦项目中引入spring-boot-starter-security启动器,安全功能会立即生效,启动项目后控制台会打印Using generated security password:...,这个即为默认密码,默认用户名为“user”。登录首页会跳转到自带的登录页面,输入该账号密码即可进入。

二、自定义用户认证

通过自定义WebSecurityConfigurerAdapter类型的Bean组件重写void configure(AuthenticationManagerBuilder auth)方法,实现自定义用户认证。

Spring Security提供了多种自定一认证方式:

  • 内存身份认证(In-Memory Authentication)
  • JDBC身份认证(JDBC Authentication)
  • LDAP身份认证(LDAP Authentication)
  • 身份认证提供商(Authentication Provider)
  • 身份详情服务(UserDetailsService)

1. 内存身份认证

最简单的认证方式,一般用于测试,需要重写configure(AuthenticationManagerBuilder auth)方法。

package com.xc.config;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * @author wyp
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //设置密码编码器
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        //设置内存用户信息
        auth.inMemoryAuthentication().passwordEncoder(encoder)
                .withUser("zs").password(encoder.encode("123456")).roles("common")
                .and()
                .withUser("ls").password(encoder.encode("123456")).roles("vip");
    }
}
  1. @EnableWebSecurity注解开启MVC Security安全支持,等同于@EnableGlobalAuthentication、@Configuration、@Import的组合使用。
  2. 从Spring Security 5开始,自定义用户认证必须设置密码编码器用于保护密码,否则会异常。
  3. Spring Security有多种编码器,BCryptPasswordEncoder、Pbkdf2PasswordEncoder、SCryptPasswordEncoder等。
  4. 自定义用户时可定义用户角色roles和权限authorities,也可以一次添加多个角色或权限,如roles("common","vip")authorities("ROLE_common","ROLE_vip")是等效的。

2. JDBC 身份认证

2.1 数据准备

创建三个表customer_tb(用户表)、authority_tb(权限表)、customer_authority_tb(用户权限关联表)。

CREATE TABLE `authority_tb`  (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `authority` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `authority_tb` VALUES (1, 'ROLE_common');
INSERT INTO `authority_tb` VALUES (2, 'ROLE_vip');

SET FOREIGN_KEY_CHECKS = 1;
CREATE TABLE `customer_tb`  (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `username` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `password` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `valid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '校验用户是否合法,默认合法1',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `customer_tb` VALUES (1, 'zs', '$2a$10$X5/MLB1vMYOAF9./ib9aROrmeaoBLuvHxSw9XPoMLDJCgrjInofty', 1);
INSERT INTO `customer_tb` VALUES (2, 'ls', '$2a$10$X5/MLB1vMYOAF9./ib9aROrmeaoBLuvHxSw9XPoMLDJCgrjInofty', 1);

SET FOREIGN_KEY_CHECKS = 1;
CREATE TABLE `customer_authority_tb`  (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `customer_id` int(10) NULL DEFAULT NULL COMMENT '用户id',
  `authority_id` int(10) NULL DEFAULT NULL COMMENT '权限id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `customer_authority_tb` VALUES (1, 1, 1);
INSERT INTO `customer_authority_tb` VALUES (2, 2, 2);

SET FOREIGN_KEY_CHECKS = 1;

注意: 由于Security进行用户查询时是先通过username定位用户是否唯一的,所以customer_tb的username字段必须唯一;customer_tb必须定义一个tinyint类型的字段(对应boolean类型的属性,如上面创建的valid),用于校验用户身份是否合法;插入的密码必须对应编码器编码后的密码;customer_tb的权限值必须带有“ROLE_”前缀,而默认的用户角色值则是对应权限去掉前缀“ROLE_”。

2.2 添加JDBC启动器

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

此处省略全局配置…

2.3 使用JDBC进行身份认证

package com.xc.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import javax.sql.DataSource;

/**
 * 注解:@EnableWebSecurity-开启MVC Security安全支持
 * @author wyp
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //设置密码编码器
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        //使用JDBC进行身份验证
        String userSql = "select username,password,valid from customer_tb where username = ?";
        String authoritySql = "select c.username,a.authority " +
                "from customer_tb c,authority_tb a,customer_authority_tb ca " +
                "where ca.customer_id=c.id and ca.authority_id=a.id and c.username=?";
        auth.jdbcAuthentication().passwordEncoder(encoder)
                .dataSource(dataSource)
                .usersByUsernameQuery(userSql)
                .authoritiesByUsernameQuery(authoritySql);
    }
}
  1. 定义用户查询的sql语句时,必须返回用户名username、密码password、是否为有效用户valid三个字段信息;
  2. 定义权限查询的sql语句时,必须返回用户名username、权限authority两个字段信息;
  3. usersByUsernameQuery(String query)设置用于通过用户名查找用户的查询;
  4. authoritiesByUsernameQuery(String query)设置用于通过用户名查找用户权限的查询。

频繁的使用jdbc进行数据查询认证麻烦且会降低网站的响应速度,推荐使用UserDetailsService身份认证。

3. UserDetailsService 身份认证

如某些业务已经实现了用户信息的查询服务,就没必要再使用JDBC进行身份认证了。

3.1 定义用户与角色信息的查询接口

CustomerServiceImpl.java

package com.xc.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xc.entity.Authority;
import com.xc.entity.Customer;
import com.xc.entity.CustomerAuthority;
import com.xc.mapper.AuthorityMapper;
import com.xc.mapper.CustomerAuthorityMapper;
import com.xc.mapper.CustomerMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
 * @author wyp
 */
@Service
public class CustomerServiceImpl implements CustomerService{

    @Autowired
    private CustomerMapper customerMapper;
    @Autowired
    private AuthorityMapper authorityMapper;
    @Autowired
    private CustomerAuthorityMapper customerAuthorityMapper;

    @Resource
    private RedisTemplate<String,Object> redisTemplate;

    @Override
    public Customer findCustomerByName(String username) {
        Customer customer;
        Object o = redisTemplate.opsForValue().get("customer::" + username);
        if (o != null) {
            customer = (Customer) o;
        }else {
            QueryWrapper<Customer> customerQueryWrapper = new QueryWrapper<>();
            customerQueryWrapper.eq("username",username);
            customer = customerMapper.selectOne(customerQueryWrapper);
            if (customer != null) {
                redisTemplate.opsForValue().set("customer::" + username,customer);
            }
        }
        return customer;
    }

    @Override
    public List<Authority> findAuthorityByName(String username) {
        List<Authority> authorityList;
        Object o = redisTemplate.opsForValue().get("authorityList::" + username);
        if (o != null) {
            authorityList = (List<Authority>) o;
        }else {
            authorityList = ...; //此处省略查询步骤
            if (authorityList.size()>0) {
                redisTemplate.opsForValue().set("authorityList::" + username,authorityList);
            }
        }
        return authorityList;
    }
}

3.2 定义UserDetailsService的实现类来封装认证用户信息

UserDetailsService是Security提供的用于封装认证用户信息的接口,通过重写loadUserByUsername(String username)方法通过用户名加载用户信息。

package com.xc.config.service;

import com.xc.entity.Authority;
import com.xc.entity.Customer;
import com.xc.service.CustomerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

/**
 * @author wyp
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private CustomerService customerService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //获取用户和权限信息
        Customer customer = customerService.findCustomerByName(username);
        //如果用户不存在,必须抛出异常,否则会导致整体报错
        if (customer == null) {
            throw new UsernameNotFoundException("当前用户不存在");
        }
        List<Authority> authorityList = customerService.findAuthorityByName(username);
        //对用户权限进行封装
        List<SimpleGrantedAuthority> list = authorityList.stream()
                .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())).collect(Collectors.toList());
        //返回封装的UserDetails用户详情类
        return new User(customer.getUsername(), customer.getPassword(), list);
    }
}

3.3 实现UserDetailsService身份认证

package com.xc.config;

import com.xc.config.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;


/**
 * 注解:@EnableWebSecurity-开启MVC Security安全支持
 * @author wyp
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //设置密码编码器
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        //使用userDetailsService进行身份认证
        auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
    }
}

三、自定义用户授权

HttpSecurity类的主要方法及说明

方法说明
authorizeRequests()开启基于HttpServletRequest请求访问的限制
formLogin()开启基于表单的用户登录
httpBasic()开启基于HTTP请求的Basic认证登录
logout()开启退出登录的支持
sessionManagement()开启Session管理配置
rememberMe()开启记住我功能
csrf()配置CSRF跨站请求伪造防护功能

1.1 自定义用户访问控制

通过authorizeRequests()开启

用户访问控制主要方法及说明

方法描述
antMatchers(String… antPatterns)开启Ant风格的路径匹配
mvcMatchers(String… mvcPatterns)开启MVC风格的路径匹配
regexMatchers(String… regexPatterns)开启正则表达式的路径匹配
and()功能连接符
anyRequest()匹配任何请求
rememberMe()开启记住我功能
access(String attribute)匹配给定的SpEL表达式计算结果是否为true
hasAnyRole(String… authorities)匹配用户是否有参数中的任意角色
hasRole(String role)匹配用户是否有某一个角色
hasAnyAuthority(String… authorities)匹配用户是否有参数中的任意权限
hasAuthority(String authority)匹配用户是否有某一权限
authenticated()匹配已经登录认证的用户
fullyAuthenticated()匹配完整登录认证的用户,而非记住我登录用户
hasIpAddress(String ipaddressExpression)匹配某IP地址的请求访问
permitAll()无条件对请求进行放行

示例:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/detail/common/**").hasRole("common")
                .antMatchers("/detail/vip/**").hasRole("vip")
            	//其他请求要求用户必须进行登录认证
                .anyRequest().authenticated()
                .and()
                .formLogin();
    }
}

1.2 自定义用户登录

之前一直使用的自带的登录界面,但通常登录界面要自己定制。

通过formLogin()开启。

用户登录主要方法及说明

方法描述
loginPage(String loginPage)用户登录页面跳转路径,默认为get请求的/login
successForwardUrl(String forwardUrl)用户登录成功后的重定向地址
successHandler(AuthenticationSuccessHandler successHandler)用户登录成功后的处理
defaultSuccessUrl(String defaultSuccessUrl)用户直接登录后默认跳转地址
failureForwardUrl(String forwardUrl)用户登录失败后的重定向地址
failureUrl(String authenticationFailureUrl)用户登录失败后的跳转地址,默认/login?error
failureHandler(AuthenticationFailureHandler authenticationFailureHandler)用户登录失败后的处理结果
usernameParameter(String usernameParameter)登录用户的用户名参数,默认username
passwordParameter(String passwordParameter)登录用户的密码参数,默认password
loginProcessingUrl(String loginProcessingUrl)登录表单提交的路径,默认为post请求的/login
permitAll()无条件对请求进行放行

resources目录的templates下新建login目录,创建login.html,静态资源(图片css等)放在static/login下

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <link rel="stylesheet" th:href="@{/login/css/login.css}">
</head>
<body th:style="'background-image: url(/login/img/back.jpg)'">
<div>
    <form th:action="@{/userLogin}" method="post" style="width: 500px;margin: 200px auto;border: 2px gray solid;text-align: center">
        <h2>登录</h2>
        <!--错误信息登录提示框-->
        <div th:if="${param.error}" style="color: red">用户名或密码错误</div>
        <label>
            <input type="text" name="name" placeholder="用户名">
        </label><br>
        <label>
            <input type="text" name="password" placeholder="密码">
        </label><br>
        <div>
            <label>
                <input type="checkbox" value="remember-me">
            </label>记住我
        </div><br>
        <input type="submit" value="登录"><br>
    </form>
</div>
</body>
</html>

其中,登录提交方式必须是post,th:if="${param.error}"判断请求中是否带有error参数,该参数的Security默认的,用户也可以自定义。

定义登录跳转控制层

@GetMapping("/userLogin")
public String toLoginPage() {
    return "login/login";
}

然后定义用户登录控制,重写configure(HttpSecurity http)方法:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        //放行静态资源
        .antMatchers("/login/**").permitAll()
        .antMatchers("/detail/common/**").hasRole("common")
        .antMatchers("/detail/vip/**").hasRole("vip")
        .anyRequest().authenticated();
    http.formLogin()
        .loginPage("/userLogin").permitAll()
        .loginProcessingUrl("/userLogin")
        .usernameParameter("name").passwordParameter("password")
        .defaultSuccessUrl("/")
        .failureUrl("/userLogin?error");
}

注意:

  • 其中loginPage(“/userLogin”)需和登录页访问路径一致
  • usernameParameter(“name”).passwordParameter(“password”)中的属性名(默认为username和password)必须与前端表单名一致
  • defaultSuccessUrl(“/”)登录成功跳转页
  • failureUrl(“/userLogin?error”)失败跳转页(默认为“/login?error”),这里的error可以让前端的th:if="${param.error}"收到

1.3 自定义用户退出

使用HttpSecurity类的logout()方法处理用户退出,会清除Session和记住我等任何默认用户配置。

用户退出主要方法及说明

方法说明
logoutUrl(String logoutUrl)用户退出处理控制URL,默认post请求的/logout
logoutSuccessUrl(String logoutSuccessUrl)用户退出成功后的重定向地址
logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler)用户退出成功后的处理器设置
deleteCookies(String… cookieNamesToClear)用户退出后删除指定的Cookie
invalidateHttpSession(boolean invalidateHttpSession)用户退出后是否立即清除Session,默认true
clearAuthentication(boolean clearAuthentication)用户退出后是否立即清除Authentication用户认证信息,默认true

首页添加注销按钮:

<form th:action="@{/myLogout}" method="post">
    <input type="submit" value="注销">
</form>

在configure(HttpSecurity http)方法中新增退出:

http.logout()
    .logoutUrl("/myLogout")
    .logoutSuccessUrl("/");

1.4 登录用户信息获取

获取用户信息通常用以下两种方式:HttpSession和SecurityContextHolder。(需要关闭csrf防护http.csrf().disable();

使用HttpSession获取用户信息:

@GetMapping("/getUserBySession")
public void getUser(HttpSession session) {
    // 从当前HttpSession获取绑定到此会话的所有对象的名称
    Enumeration<String> names = session.getAttributeNames();
    while (names.hasMoreElements()) {
        // 获取HttpSession中会话名称
        String element = names.nextElement();
        // 获取HttpSession中的应用上下文
        SecurityContextImpl attribute = (SecurityContextImpl) session.getAttribute(element);
        System.out.println("element: " + element);
        System.out.println("attribute: " + attribute);
        // 获取用户相关信息
        Authentication authentication = attribute.getAuthentication();
        UserDetails principal = (UserDetails) authentication.getPrincipal();
        System.out.println(principal);
        System.out.println("username: " + principal.getUsername());
    }
}

会话中有一个key为“SPRING_SECURITY_CONTEXT”的用户信息(即element输出的信息),UserDetails中封装了用户的主要信息,如子用户名、权限等。

使用SecurityContextHolder获取用户信息:

@GetMapping("/getUserByContext")
public void getUserByContext() {
    //获取应用上下文
    SecurityContext context = SecurityContextHolder.getContext();
    System.out.println("context = " + context);
    //获取用户相关信息
    Authentication authentication = context.getAuthentication();
    UserDetails userDetails = (UserDetails) authentication.getPrincipal();
    System.out.println("userDetails = " + userDetails);
    System.out.println("username = " + userDetails.getUsername());
}
 类似资料: