1、SpringSecurity 的简介
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,它是 Spring 项目组中用来提供安全认证服务的框架,能够为基于 Sprin g的企业应用系统提供声明式的安全访问控制解决方案。
Spring Security 的前身是 Acegi Security。它是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。Spring Security 采用了 AOP(面向切面编程)思想,并基于 Servlet 过滤器实现。
下面将介绍 Spring Boot 整合 Spring Security 实现一个简单的认证与授权应用,执行结果如下如:
(1)登录页面
(2)登录成功后,跳转至首页:
2、数据库准备
使用 MySQL 数据库,设计一个自定义的数据表结构,并添加数据。
-- 创建数据库
CREATE DATABASE IF NOT EXISTS db_admin;
USE db_admin;
-- 创建自定义的用户信息表
DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info(
id BIGINT(20) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
username VARCHAR(50) NOT NULL COMMENT '用户名称',
PASSWORD VARCHAR(60) COMMENT '用户密码',
ENABLE TINYINT(4) NOT NULL DEFAULT 1 COMMENT '是否启用',
roles VARCHAR(100) COMMENT '用户角色,多个角色之间用英文逗号分割',
KEY key_username (username)
);
-- 插入数据
INSERT INTO user_info(username,PASSWORD,ENABLE,roles) VALUES('admin','123456',1,'ROLE_ADMIN,ROLE_USER');
INSERT INTO user_info(username,PASSWORD,ENABLE,roles) VALUES('user','123456',1,'ROLE_USER');
INSERT INTO user_info(username,PASSWORD,ENABLE,roles) VALUES('panjunbiao','123456',1,'ROLE_USER');
-- 查询
SELECT * FROM user_info;
3、创建项目
【示例】SpringBoot 整合 SpringSecurity 创建一个简单的认证应用。
3.1 创建 Spring Boot 项目
创建 SpringBoot 项目,项目结构如下图:
3.1 添加 Maven 依赖
在 pom.xml 配置文件中添加 Spring Security、MyBatis、JDBC、Thymeleaf 模板引擎、Lombok 依赖。
<!-- Spring Security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.18</version>
</dependency>
<!-- MyBatis 与 Spring Boot 整合依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- MySQL 的 JDBC 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 引入Thymeleaf模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
3.3 配置数据库连接参数
在项目的 application.yml 文件中,添加数据库连接的配置。
spring:
# 使用Thymeleaf模板引擎
thymeleaf:
mode: HTML5
encoding: UTF-8
cache: false
servlet:
content-type: text/html
# 数据库连接
datasource:
url: jdbc:mysql://localhost:3306/db_admin?useSSL=false&&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
4、整合 MyBatis 框架实现持久化
4.1 创建实体类(Entity 层)
在项目的 entity 层,创建 UserInfo 类(用户信息实体类),并继承 UserDetails 类。
package com.pjb.securitydemo.entity;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
/**
* 用户信息实体类
*/
@Data
public class UserInfo implements UserDetails
{
private Long id; //主键ID
private String username; //用户名称
private String password; //用户密码
private String roles; //用户角色
private boolean enable; //是否启用
private List<GrantedAuthority> authoritys; //权限集合
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
{
return this.authoritys;
}
public void setAuthoritys(List<GrantedAuthority> authoritys)
{
this.authoritys = authoritys;
}
@Override
public boolean isAccountNonExpired()
{
return true;
}
@Override
public boolean isAccountNonLocked()
{
return true;
}
@Override
public boolean isCredentialsNonExpired()
{
return true;
}
@Override
public boolean isEnabled()
{
return true;
}
}
实现 UserDetails 定义的几个方法:
- isAccountNonExpired、isAccountNonLocked 和 isCredentialsNonExpired 暂且用不到,统一返回 true,否则 Spring Security 会认为账号异常。
- isEnabled 对应 enable 字段,将其代入即可。
- getAuthorities 方法本身对应的是 roles 字段,但由于机构不一致,所以此次新建一个,并在后续进行填充。
4.2 Mapper动态代理接口(Mapper层)
在 Mapper层,创建 UserInfoMapper 类(用户信息Mapper动态代理接口)
package com.pjb.securitydemo.mapper;
import com.pjb.securitydemo.entity.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
/**
* 用户信息Mapper动态代理接口
**/
@Repository
@Mapper
public interface UserInfoMapper
{
@Select("SELECT * FROM user_info WHERE username=#{username}")
UserInfo findUserName(@Param("username") String username);
}
5、整合 Spring Security 框架实现认证与授权
5.1 服务类(Service 层)
创建 LoginService 类(登录服务类),实现 UserDetailsService 接口,重写 loadUserByUsername 方法,实现登录认证功能。
package com.pjb.securitydemo.service;
import com.pjb.securitydemo.entity.UserInfo;
import com.pjb.securitydemo.mapper.UserInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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;
/**
* 登录服务类
*/
@Service
public class LoginService implements UserDetailsService
{
@Autowired
private UserInfoMapper userInfoMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
//获取用户信息
UserInfo userInfo = userInfoMapper.findUserName(username);
//用户不存在,抛出异常
if (userInfo == null)
{
throw new UsernameNotFoundException("用户不存在");
}
//将数据库形式的 roles 解析为 UserDetails 的权限集
// AuthorityUtils.commaSeparatedStringToAuthorityList 方法是 Spring Security
//提供的,该方法用于将逗号分割的权限集字符串切割成可用权限集对象列表
List<GrantedAuthority> grantedAuthorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(userInfo.getRoles());
userInfo.setAuthoritys(grantedAuthorityList);
return userInfo;
}
}
5.2 处理类(Handler 层)
(1)登录成功处理类
package com.pjb.securitydemo.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登录成功处理类
*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler
{
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException
{
//重定向至首页
httpServletResponse.sendRedirect("/");
}
}
(2)登录失败处理类
package com.pjb.securitydemo.handler;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 登录失败处理类
*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler
{
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException authenticationException) throws IOException, ServletException
{
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write("登录失败");
}
}
(3)403无权限处理类
package com.pjb.securitydemo.handler;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 403无权限处理类
*/
@Component
public class PermissionDeniedHandler implements AccessDeniedHandler
{
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException
{
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write("403无权限");
}
}
5.3 配置类(Config 层)
创建 WebSecurityConfig 类(Spring Security 配置类),并添加 @EnableWebSecurity 注解和继承 WebSecurityConfigurerAdapter 类。
package com.pjb.securitydemo.config;
import com.pjb.securitydemo.handler.LoginFailureHandler;
import com.pjb.securitydemo.handler.LoginSuccessHandler;
import com.pjb.securitydemo.handler.PermissionDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* Spring Security 配置类
* @author pan_junbiao
**/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private PermissionDeniedHandler permissionDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.authorizeRequests() //返回一个URL拦截注册器
.antMatchers("/admin/api/**").hasRole("ADMIN") //设置授权角色
.antMatchers("/user/api/**").hasRole("USER") //设置授权角色
.antMatchers("/app/api/**", "/captcha.jpg").permitAll() //公开其权限
.anyRequest() //匹配所有的请求
.authenticated() //所有匹配的URL都需要被认证才能访问
.and() //结束当前标签,让上下文回到 HttpSecurity
.formLogin() //启动表单认证
.loginPage("/myLogin.html") //自定义登录页面
.loginProcessingUrl("/auth/form") //指定处理登录请求路径
.permitAll() //使登录页面不设限访问
//.defaultSuccessUrl("/index") //登录认证成功后的跳转页面
.successHandler(loginSuccessHandler) //指定登录成功时的处理
.failureHandler(loginFailureHandler) //指定登录失败时的处理
.and()
.exceptionHandling().accessDeniedHandler(permissionDeniedHandler) //403无权时的返回操作
.and().csrf().disable(); //关闭CSRF的防御功能
}
/**
* 由于5.x版本之后默认启用了委派密码编译器,
* 因而按照以往的方式设置内存密码将会读取异常,
* 所以需要暂时将密码编码器设置为 NoOpPasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder()
{
return NoOpPasswordEncoder.getInstance();
}
}
6、前端页面
6.1 控制器层(Controller层)
创建 IndexController 类(首页控制器),实现获取当前登录用户名并跳转至首页。
package com.pjb.securitydemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
/**
* 首页控制器
* @author pan_junbiao
**/
@Controller
public class IndexController
{
/**
* 首页
*/
@RequestMapping("/")
public String index(HttpServletRequest request)
{
//获取当前登录人
String userName = "未登录";
Principal principal = request.getUserPrincipal();
if(principal!=null)
{
userName = principal.getName();
}
//返回页面
request.setAttribute("userName",userName);
return "/index.html";
}
}
6.2 编写登录页面
在 resources\static 静态资源目录下,创建 myLogin.html 页面。
注意:myLogin.html 页面必须放在 resources\static 静态资源目录下,否则页面无法加载。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
<meta name="author" content="pan_junbiao的博客">
</head>
<body>
<form name="myForm" action="/auth/form" method="post">
<table align="center">
<caption>用户登录</caption>
<tr>
<td>登录账户:</td>
<td>
<input type="text" name="username" placeholder="请输入登录账户" value="panjunbiao" />
</td>
</tr>
<tr>
<td>登录密码:</td>
<td>
<input type="password" name="password" placeholder="请输入登录密码" value="123456" />
</td>
</tr>
<!-- 以下是提交、取消按钮 -->
<tr>
<td colspan="2" style="text-align: center; padding: 5px;">
<input type="submit" value="提交" />
<input type="reset" value="重置" />
</td>
</tr>
</table>
</form>
</body>
</html>
6.3 编写首页
在 resources\templates 资源目录下,创建 index.html 页面。
注意:首页 index.html 页面中使用 Thymeleaf 模板 。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
<meta name="author" content="pan_junbiao的博客">
</head>
<body>
<h1 style="color: red">Hello,Spring Security</h1>
<p>博客信息:您好,欢迎访问 pan_junbiao的博客</p>
<p>博客地址:https://blog.csdn.net/pan_junbiao</p>
<p th:text="'当前登录人:' + ${userName}"></p>
<a href="/logout" onclick="return confirm('确认注销吗?');">登出</a>
</body>
</html>