场景描述
最经在学习 SpringSecurity
安全框架。记录一下如何从零将SpringSecurity
与SpringBoot
一步一步构建进去。大概的搭建步骤是:
- 首先,搭建一个纯
SpringBoot
框架搭建项目进行浏览器页面访问。会详细记录搭建流程。 - 第二,完成纯框架搭建后便是简单整合
SpringBoot + SpringSecurity
后的效果。 - 第三,简单整合后便是
SpringBoot + SpringSecurity + Thymeleaf
整合。这一步会整合进去前端页面效果。
描述:分步骤进行整合的目的就是能够更直观的对比整合前后的效果。方便日后可以更快速的进行框架的搭建。
一、纯 SpringBoot 框架搭建项目
- 用
idea
快发工具构建一个SpringBoot
项目,构建步骤如下截图。
注意:点击完成后会idea
工具会使用maven
自动创建项目。但是有时候pom
爆红。有可能是以下几个原因。得自己设置好maven
仓库,最好在配置文件配置好阿里云的镜像仓库。还有就是idea
工具缓存的问题,导致无法加载到自己需要的SpringBoot
版本。解决办法就是清除缓存加重启idea
。最后都没用的话可以试着去maven
仓库找到有的版本进行依赖的配置。项目构建完没问题的如下图所示。
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.study</groupId>
<artifactId>security_demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security_demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 编写
TestController
类。存放路径:com.study.security_demo.controller.TestController
@RestController
public class TestController {
@RequestMapping("/demo")
public String hello(){
return "hello,Spring Security";
}
}
- 启动
SpringBoot
项目。浏览器访问路径:http://localhost:8080/demo
提示:控制台没有报错就说明启动成功。
上图是正常运行后浏览器的访问结果。这也是没有做权限控制的访问结果。
二、简单整合 SpringBoot + SpringSecurity 步骤
- 首先我们需要添加
SpringSecurity
的pom
文件的依赖
<!-- Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 刷新依赖,重启项目。然后我们再去访问http://localhost:8080/demo,会得到如下页面。
描述:这时
SpringSecurity
会直接接过访问权限。重定向到它自定的一个页面,让我们登陆。有默认的用户名user、密码是控台输出的一段由uuid
生成的字符序列。如下图所示:
源码的是在这个类:
org.springframework.boot.autoconfigure.security.SecurityProperties
描述:这是你输入用户名跟密码就可以访问刚才的资源了。比如我的是:
用户名:user
密码:a0e69fbe-abe5-4e9d-beba-86cf7df70b13
你想知道你的就去idea
控制台找到那段字符序列输入即可。
- 上一步是按照默认的方式,如果我们想要配置自己的用户名跟密码进行登陆限制,那就在
resources
文件夹下的application.properties配置文件配置如下内容即可。
# 通过配置文件设置可以登陆的用户名跟密码
spring.security.user.name=admin
spring.security.user.password=123456
描述:
配置完后就可以用你自己的设置的用户名密码进行登陆了。
这便是简单的整合SpringBoot + SpringSecurity
三、SpringBoot + SpringSecurity + Thymeleaf 整合
描述:这一步主要是为了整合进去
html
,做到对访问路径的访问控制。也就是说不同的登陆用户对于相同的页面有不同的访问权限。方便更好的理解SpringSecurity
权限控制。
- 首先我们需要先引入用到的相关依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--Thymeleaf整合SpringBoot依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--对Thymeleaf添加Spring Security标签支持-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!--开发的热加载配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
- 为了方便测试我们先关闭
Thymeleaf
的缓存。
# 关闭 thymeleaf 引擎缓存
spring.thymeleaf.cache=false
- 整合好相关的资源文件。文件目录如下:
描述:
上述就是要整合的资源文件,我会在下面放入网盘链接,方便各位学习下载使用。拿到文件后直接拷贝到相关目录即可。下面我会分析各个文件的作用。
运行效果:
描述:重启项目。然后我们再去访问:http://localhost:8080
会得到如下页面:
描述:上图是首页面,有导航栏,有7个超链接。分别对应跳转不同页面。需要登录点击登录
。
描述:上图是登录页面有4个账户可以登陆,不同的账户对应这不同页面访问权限。
4个账户分别是:a\a、b\b、c\c、d\d。
描述:上图是a账户登录可以访问的页面显示。
描述:无权限的访问页如示
如果没有登录是只能访问首页,访问其他页面都会跳转到登录。
下面看下实现方式。
首先这里定义了五个自定义权限注解,分别是:
com.study.security_demo.annotations.IsAdmin
com.study.security_demo.annotations.IsEditor
com.study.security_demo.annotations.IsReviewer
com.study.security_demo.annotations.IsUser
com.study.security_demo.annotations.MyPermissions
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public @interface IsAdmin {
}
描述:我们拿其中一个注解的源码看下,
@Target
、@Retention
是java
元注解。@Target
是用于标注该注解使用方式:METHOD 是用于方法上、TYPE 是用于类上。@Retention
该注解表示在运行时环境生效。@PreAuthorize
是SpringSecurity
的注解,只有一个值,表示定义的权限规则。hasAnyRole('ROLE_ADMIN')
表示拥有任何一个权限既有效。
我们在开发网站的过程中,比如GET /user/editor
这个请求角色为EDITOR
和ADMIN
肯定都可以,如果我们在每一个需要判断权限的方法上面写一长串的权限表达式,一定很复杂。但是通过自定义权限注解,我们可以通过@IsEditor
这样的方法来判断,这样一来就简单了很多。
其次,需要继承一个 User
类,定义一个 CustomUser
类。
org.springframework.security.core.userdetails.User;
继承该类。
public class CustomUser extends User {
private int id;
public CustomUser(int id, String username, String password,
Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
描述:继承后我们就可以利用这个类定义自己需要的账户,用于访问登录了。
接下来,我是用map
集合来模拟数据库数据,定义一个 Database
类。
public class Database {
private Map<String, CustomUser> data;
private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
public Database() {
data = new HashMap<>();
CustomUser a = new CustomUser(1, "a", getPassword("a"), getGrants("ROLE_USER"));
CustomUser b = new CustomUser(2, "b", getPassword("b"), getGrants("ROLE_EDITOR"));
CustomUser c = new CustomUser(3, "c", getPassword("c"), getGrants("ROLE_REVIEWER"));
CustomUser d = new CustomUser(4, "d", getPassword("d"), getGrants("ROLE_ADMIN"));
data.put("a", a);
data.put("b", b);
data.put("c", c);
data.put("d", d);
}
public Map<String, CustomUser> getDatabase() {
return data;
}
private String getPassword(String raw) {
return passwordEncoder.encode(raw);
}
private Collection<GrantedAuthority> getGrants(String role) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(role);
}
}
再接着,需要继承一个 WebSecurityConfigurerAdapter
类,定义一个 SecurityConfig
类。
org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
继承该类。
/**
* 开启方法注解支持,我们设置prePostEnabled = true是为了后面能够使用hasRole()这类表达式
*
*/
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* TokenBasedRememberMeServices的生成密钥
*/
private final String SECRET_KEY = "123456";
@Autowired
private CustomUserDetailsService customUserDetailsService;
/**
* 必须有此方法,Spring Security官方规定必须要有一个密码加密方式。
* 注意:例如这里用了BCryptPasswordEncoder()的加密方法,那么在保存用户密码的时候也必须使用这种方法,确保前后一致。
* 详情参见项目中Database.java中保存用户的逻辑
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置Spring Security,下面说明几点注意事项。
* 1. Spring Security 默认是开启了CSRF的,此时我们提交的POST表单必须有隐藏的字段来传递CSRF,
* 而且在logout中,我们必须通过POST到 /logout 的方法来退出用户,详见我们的login.html和logout.html.
* 2. 开启了rememberMe()功能后,我们必须提供rememberMeServices,例如下面的getRememberMeServices()方法,
* 而且我们只能在TokenBasedRememberMeServices中设置cookie名称、过期时间等相关配置,如果在别的地方同时配置,会报错。
* 错误示例:xxxx.and().rememberMe().rememberMeServices(getRememberMeServices()).rememberMeCookieName("cookie-name")
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login") // 自定义用户登入页面
.failureUrl("/login?error") // 自定义登入失败页面,前端可以通过url中是否有error来提供友好的用户登入提示
.and()
.logout()
.logoutUrl("/logout")// 自定义用户登出页面
.logoutSuccessUrl("/")
.and()
.rememberMe() // 开启记住密码功能
.rememberMeServices(getRememberMeServices()) // 必须提供
.key(SECRET_KEY) // 此SECRET需要和生成TokenBasedRememberMeServices的密钥相同
.and()
/*
* 默认允许所有路径所有人都可以访问,确保静态资源的正常访问。
* 后面再通过方法注解的方式来控制权限。
*/
.authorizeRequests().anyRequest().permitAll()
.and()
.exceptionHandling().accessDeniedPage("/403"); // 权限不足自动跳转403
}
/**
* 如果要设置cookie过期时间或其他相关配置,请在下方自行配置
*/
private TokenBasedRememberMeServices getRememberMeServices() {
TokenBasedRememberMeServices services = new TokenBasedRememberMeServices(SECRET_KEY, customUserDetailsService);
services.setCookieName("remember-cookie");
services.setTokenValiditySeconds(100); // 默认14天
return services;
}
}
描述:具体描述可以看注释了。开启方法注解支持:只需要在类上添加
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) 注解,我们设置 prePostEnabled = true
是为了支持hasRole()
这类表达式。
还有页面类 UserService
类。
@Service
public class UserService {
private Database database = new Database();
public CustomUser getUserByUsername(String username) {
CustomUser originUser = database.getDatabase().get(username);
if (originUser == null) {
return null;
}
/*
* 此处有坑,之所以这么做是因为Spring Security获得到User后,会把User中的password字段置空,以确保安全。
* 因为Java类是引用传递,为防止Spring Security修改了我们的源头数据,所以我们复制一个对象提供给Spring Security。
* 如果通过真实数据库的方式获取,则没有这种问题需要担心。
*/
return new CustomUser(originUser.getId(), originUser.getUsername(), originUser.getPassword(), originUser.getAuthorities());
}
}
还需要继承一个 UserDetailsService
类,定义一个 CustomUserDetailsService
类。
org.springframework.security.core.userdetails.UserDetailsService;
继承该类。
/**
* 实现官方提供的UserDetailsService接口即可
*/
@Service
public class CustomUserDetailsService implements UserDetailsService {
private Logger LOGGER = LoggerFactory.getLogger(getClass());
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
CustomUser user = userService.getUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("该用户不存在");
}
LOGGER.info("用户名:"+username+" 角色:"+user.getAuthorities().toString());
return user;
}
}
在定义三个controller
类
UserController
@IsUser // 表明该控制器下所有请求都需要登入后才能访问
@Controller
@RequestMapping("/user")
public class UserController {
@GetMapping("/home")
public String home(Model model) {
// 方法一:通过SecurityContextHolder获取
CustomUser user = (CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
model.addAttribute("user", user);
return "user/home";
}
@GetMapping("/editor")
@IsEditor
public String editor(Authentication authentication, Model model) {
// 方法二:通过方法注入的形式获取Authentication
CustomUser user = (CustomUser) authentication.getPrincipal();
model.addAttribute("user", user);
return "user/editor";
}
@GetMapping("/reviewer")
@IsReviewer
public String reviewer(Principal principal, Model model) {
// 方法三:同样通过方法注入的方法,注意要转型,此方法很二,不推荐
CustomUser user = (CustomUser) ((Authentication) principal).getPrincipal();
model.addAttribute("user", user);
return "user/reviewer";
}
@GetMapping("/admin")
@IsAdmin
public String admin() {
// 方法四:通过Thymeleaf的Security标签进行,详情见admin.html
return "user/admin";
}
@GetMapping("/perm")
@MyPermissions
public String perm() {
// 方法四:通过Thymeleaf的Security标签进行,详情见admin.html
return "user/perm";
}
}
描述:用户访问控制器,根据不同访问路径进行
html
页面的访问。
IndexController
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "index/index";
}
@GetMapping("/login")
public String login() {
return "index/login";
}
@GetMapping("/logout")
public String logout() {
return "index/logout";
}
}
描述:登录访问控制器,主页、登录页、退出页。
ErrorController
@Controller
public class ErrorController {
@GetMapping("/404")
public String handle404() {
return "404";
}
@GetMapping("/403")
public String handle403() {
return "403";
}
@GetMapping("/500")
public String handle500() {
return "500";
}
}
描述:错误页面访问控制器。
最后总结:
源代码我会上传到csdn
,需要的可以直接下载学习。如果对你有帮助的话可以麻烦给个小小的赞,举手之劳便是对博主最大的鼓励,谢谢。
源代码链接