第1部分 Spring基础
第4章 使用非关系型数据
关系型数据库一直是首选,近年来"NoSQL"数据库提供了数据存储的不同概念和结构。
SpringData为很多NoSQL数据库提供了支持,包括MongoDB、Cassandra、Couchbase、Neo4j、Redis等,无论选择哪种,编程模型几乎是相同的。
看两个最流行的NoSQL数据库:Cassandra和MongoDB。
4.1 使用Cassandra存储库
Cassandra是一个分布式、高性能、始终可用、最终一致、列分区存储的NoSQL数据库。
Cassandra处理的是要写入表中的数据行,这些数据会被分区到一对多的分布式节点上。
没有任何一个节点会持有所有的数据,任何给定的数据行都会跨多个节点保存副本,从而消除单点故障。
Spring Data Cassandra为Cassandra数据库提供了自动化存储库的支持,与Spring Data JPA对关系型数据库的支持非常类似,还提供了用于将应用的领域模型映射为后端数据库结构的注解。
要完整了解它的特定,建议去阅读Cassandra的官方文档。
4.1.1 启动Spring Data Cassandra
其他略,用到时再说。
4.2 编写MongoDB存储库
自己查吧,用Docker容器来启动。
第5章 保护Spring
5.1 启用Spring Security
自动或手动添加Spring Boot security starter依赖到pom.xml文件中。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
启动,访问主页,http://localhost:8080/login
用户名:user,密码在启动时的控制台:1a6d18d5-a6b9-4aa1-bcbe-08fef7cd5c17
登录进去。
5.2 配置Spring Security
Spring Security有多种配置方式,冗长的基于XML配置,现在都支持基于Java的配置,更加容易编写和阅读。
Spring Security的基础配置类
package tacos.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
基础的安全配置主要工作声明 PasswordEncoder bean,创建新用户和登录时对用户认证都会用到它,本例使用BCryptPasswordEncoder。
Spring Security提供的密码转换器:
- BCryptPasswordEncoder:使用bcrypt强哈希加密。
- NoOpPasswordEncoder:不使用任何转码,没有任何加密技术,不适合生产环境使用。
- Pbkdf2PasswordEncoder:使用PBKDF2加密。
- SCryptPasswordEncoder:使用Scrypt哈希加密。
- StandardPasswordEncoder:使用SHA-256哈希加密,被认为加密不够安全,已被废弃。
无论哪种密码转换器,数据库中的密码永远不会被解码,用户登录时加密后与数据库对比。
是PasswordEncoder的match()方法中进行的。
Spring Security提供了多个内置的UserDetailsService实现,包括:
- 内存用户存储
- JDBC用户存储
- LDAP用户存储
5.2.1 基于内存的用户详情服务
用户信息可以存在内存之中,假设我们有有限的用户,而且这几个用户几乎不会发生变化,这种情况可以将用户定义成安全配置的一部分是非常简单的。
在内存用户详情服务bean中声明用户
package tacos.security;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
List<UserDetails> usersList = new ArrayList<>();
usersList.add(new User("buzz",encoder.encode("buzz"),
Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))));
usersList.add(new User("woody",encoder.encode("woody"),
Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))));
return new InMemoryUserDetailsManager(usersList);
}
}
创建用户对象,都包含用户名,密码和权限列表。
用户名/密码:buzz/buzz woody/woody。可以登录。
UserDetails,User都是Spring Security提供的。
5.2.2 自定义用户认证
如需新增、移除或变更用户,我们用关系型数据库进行存储,对用户进行持久化处理。
定义用户实体
package tacos;
import java.util.Arrays;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class User implements UserDetails {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private final String username;
private final String password;
private final String fullname;
private final String street;
private final String city;
private final String state;
private final String zip;
private final String phoneNumber;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
}
public User(String username, String password, String fullname, String street, String city, String state,
String zip, String phoneNumber) {
super();
this.username = username;
this.password = password;
this.fullname = fullname;
this.street = street;
this.city = city;
this.state = state;
this.zip = zip;
this.phoneNumber = phoneNumber;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFullname() {
return fullname;
}
public String getStreet() {
return street;
}
public String getCity() {
return city;
}
public String getState() {
return state;
}
public String getZip() {
return zip;
}
public String getPhoneNumber() {
return phoneNumber;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
getAuthorities()方法应该返回用户被授予权限的一个集合,各种以is开头的方法返回布尔值,表明用户账号的可用、锁定、过期状态。(这些方法在UserDetails接口中,都返回true)
定义用户存储库接口
package tacos.data;
import org.springframework.data.repository.CrudRepository;
import tacos.User;
public interface UserRepository extends CrudRepository<User, Long>{
User findByUsername(String username);
}
除拓展所有CRUD操作外,还定义findByUsername()方法,在用户详情中会用到,以便根据用户名查找User。
SecurityConfig声明自定义用户详情服务Bean
@Bean
public UserDetailsService userDetailsService(UserRepository userRepo) {
return username -> {
User user = userRepo.findByUsername(username);
if(user != null) { return user; }
throw new UsernameNotFoundException("用户名:"+username+"找不到!");
};
}
UserDetailsService接口中只有一个方法loadUserByUsername(),则视为函数式接口,则不必实现类,而直接用lambda表达式来简化,需要传入一个username参数。还有就是此方法不能返回null,若返回则抛出异常。
注册用户
用户注册流程需要借助SpringMVC来完成。
用户登录表单类
package tacos.security;
import org.springframework.security.crypto.password.PasswordEncoder;
import tacos.User;
public class RegistrationForm {
private String username;
private String password;
private String fullname;
private String street;
private String city;
private String state;
private String zip;
private String phone;
public User toUser(PasswordEncoder passwordEncoder) {
return new User(username, passwordEncoder.encode(password),
fullname, street, city, state, zip, phone);
}
public RegistrationForm(String username, String password, String fullname, String street, String city, String state,
String zip, String phone) {
super();
this.username = username;
this.password = password;
this.fullname = fullname;
this.street = street;
this.city = city;
this.state = state;
this.zip = zip;
this.phone = phone;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getFullname() {
return fullname;
}
public void setFullname(String fullname) {
this.fullname = fullname;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getZip() {
return zip;
}
public void setZip(String zip) {
this.zip = zip;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
用户注册的控制器RegistrationController
package tacos.security;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import tacos.data.UserRepository;
@Controller
@RequestMapping("/register")
public class RegistrationController {
private UserRepository userRepo;
private PasswordEncoder passwordEncoder;
public RegistrationController(UserRepository userRepo, PasswordEncoder passwordEncoder) {
super();
this.userRepo = userRepo;
this.passwordEncoder = passwordEncoder;
}
@GetMapping
public String registerForm() {
return "registration";
}
public String processRegistration(RegistrationForm form) {
userRepo.save(form.toUser(passwordEncoder));
return "redirct:/login";
}
}
注册表单视图的Thymeleaf模版registration.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Taco Cloud</title>
</head>
<body>
<h1>Register</h1>
<img alt="" th:src="@{images/TacoCloud.png}">
<form method="post" th:action="@{/register}" id="registerForm">
<label for="username">Username:</label>
<input type="text" name="username"><br>
<label for="password">Password:</label>
<input type="password" name="password"><br>
<label for="confirm">Confirm password:</label>
<input type="password" name="confirm"><br>
<label for="fullname">Full name:</label>
<input type="text" name="fullname"><br>
<label for="street">Street:</label>
<input type="text" name="street"><br>
<label for="city">City:</label>
<input type="text" name="city"><br>
<label for="state">State:</label>
<input type="text" name="state"><br>
<label for="zip">Zip:</label>
<input type="text" name="zip"><br>
<label for="phone">Phone:</label>
<input type="text" name="phone"><br>
<input type="submit" value="Register">
</form>
</body>
</html>
应用已经有了完整用户注册和认证功能,启动还是无法进入注册页面,因为所有请求都需要认证。我们看一下Web请求如何被拦截和保护的。
5.3 保护Web请求
应用的安全需求是:用户在设计taco和提交订单之前,必须要经过认证,但主页、登录页和注册页应该对未认证的用户开发。
HttpSecurity可以配置很多功能,其中包括:
- 要求在某个请求提供服务之前,满足特定的安全条件。
- 配置自定义的登录页。
- 使用户能够退出应用。
- 预防跨站请求伪造。
配置HttpSecurity最常见的需求就是拦截请求以确保用户具备适当的权限。
5.3.1 保护请求
确保只有认证过的用户才能发起对"/design"和"/orders"的请求,而其他请求对所有用户均可用,如下配置就能实现这一点:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.cors(cors -> cors.configure(http))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/design","/orders").hasRole("USER")
.requestMatchers("/","/**").permitAll())
.formLogin(form -> form
.loginPage("/login"))
.logout(logout -> logout
.logoutUrl("/logout")
.permitAll())
.build();
}
具备ROLE_USER权限的用户才能访问"/design"和"/orders"。
其他的所有请求允许所有用户访问。
规则顺序很重要,前面的比后面的优先级高。
我用的是spring-security-config-6.4.2,规则和版本5的不同,具体API自己查吧。
可以用SpEL表达式来声明更丰富的安全规则,了解即可。
5.3.2 创建自定义的登录页
formLogin()方法告诉自定义登录页的路径,若没有经过认证并且需要登录,就会将用户重定向到该路径。
登录页很简单,只有一个视图没有其他东西,可以不写Controller,只要在WebConfig中将其声明为一个视图控制器,增加登录页面的视图控制器。
package tacos.web;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
registry.addViewController("/login");
}
}
新建login.html登录页的视图。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Taco Cloud</title>
</head>
<body>
<h1>Login</h1>
<img th:src="@{/images/TacoCloud.png}">
<div th:if=${error}>
无法登录,请检查你的用户名和密码是否正确!
</div>
<p>
还未注册,点击 <a th:href="@{/register}">这里</a>去注册!
</p>
<form action="post" th:action="@{/login}" id="loginForm">
<label for="username">用户名:</label>
<input type="text" name="username" id="username"><br>
<label for="password">密码:</label>
<input type="password" name="password" id=""password""><br>
<input type="submit" value="登录">
</form>
</body>
</html>
默认情况下,会监听"/login"路径登录请求,用户名密码分别为username和password,这也是可以配置的。
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/authenticate")
.usernameParameter("user")
.passwordParameter("pwd")
修改完就是,路径为"/authenticate",用户名密码为,user,pwd。
一般登录后导航到正在浏览的页面,如果直接访问登录页,导航至根路径,也就是主页,也可以修改默认的成功页,例如导航到"/design"。强制要求登录到这里的话,后面加true参数。
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.cors(cors -> cors.configure(http))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/design","/orders").hasRole("USER")
.requestMatchers("/","/**").permitAll())
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/design",true))
.logout(logout -> logout
.logoutUrl("/logout")
.permitAll())
.build();
}
上面的代码要和项目中的代码整体整理一下!!!
5.3.3 启用第三方认证
你可能在自己喜欢的Web站点上见过"使用微信登录",“使用QQ登录”,"使用支付宝登录"类似的内容。这种方式能让用户避免在Web站点特定的登录页上自己输入凭证信息。
这种认证基于OAuth2或OpenID Connect(OIDC)。
OAuth2是一个授权规范,用来通过第三方网站实现认证功能,OpenID Connect是另一个基于OAuth2的安全规范,用户规范化第三方认证过程中发生的交互。
自动或手动添加OAuth2客户端的starter依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
配置文件中设置通用属性
# 配置 OAuth2 客户端相关信息
spring.security.oauth2.client.registration.wechat.client-id=your_appid
spring.security.oauth2.client.registration.wechat.client-secret=your_appsecret
spring.security.oauth2.client.registration.wechat.client-name=微信登录
spring.security.oauth2.client.registration.wechat.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.wechat.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.wechat.scope=snsapi_userinfo
# 配置 OAuth2 提供者相关信息
spring.security.oauth2.client.provider.wechat.authorization-uri=https://open.weixin.qq.com/connect/qrconnect
spring.security.oauth2.client.provider.wechat.token-uri=https://api.weixin.qq.com/sns/oauth2/access_token
spring.security.oauth2.client.provider.wechat.user-info-uri=https://api.weixin.qq.com/sns/userinfo
spring.security.oauth2.client.provider.wechat.user-name-attribute=unionid
客户端ID和secret是用来标识我们的应用在微信中的凭证。
可以在微信的开发者网站新建应用来获取客户端ID和secret。
scope属性可以用来指定应用的权限范围,案例中获取用户的基本信息。
用户尝试访问需要认证的页面时,他们的浏览器会被重定向到微信,若还没有登录微信,将会看到微信登录页面,他们会被要求根据请求的权限范围对我们的应用程序授权,最后,用户被重定向到我们的应用程序,此时,他们完成了认证。
通过SecurityFilterChain来定义安全配置,还需要启用OAuth2登录。
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.cors(cors -> cors.configure(http))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/design","/orders").hasRole("USER")
.requestMatchers("/","/**").permitAll())
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/design",true))
.logout(logout -> logout
.logoutUrl("/logout"))
//.oauth2Login(t -> t.and())
.build();
}
也可以配置传统的通过用户名密码登录,可以在配置中指定登录页,或提供一个微信登录页面链接a,退出也同样重要,在HttpSecurity对象上调用logout方法。
整体略。
5.3.4 防止跨站请求伪造
跨站请求伪造(Cross-Site Request Forgery,CSRF)是一种常见的安全攻击。
它会让用户在一个恶意的Web页面上填写信息,然后自动的将表单以攻击受害者的身份提交到另外一个应用上。
为了防止此类攻击,展现表单的时候生成一个CSRF令牌(token),放到隐藏域中临时存储起来,以便后续服务器上使用。
Spring Security提供了内置的CSRF保护,默认启用,唯一要做的就是每个表单有一个名为"_csrf"的字段,它会持有CSRF令牌。
模版中会:
5.4 实现方法级别的安全
比如删除所有订单操作,只有管理员才有权限删除。
可以在配置类中添加对应权限,但是其他控制类也需要这样的操作,为了方便,启用方法上的安全防护。
@PreAuthorize(“hasRole(‘ADMIN’)”)
安全配置类上添加@EnableGlobalMethodSecurity使上面注解生效。
假设根据ID获取订单方法,想限制这个方法,可以用@PostAuthorize注解。
5.5 了解用户是谁
@AuthenticationPrincipal
想详细了解去看下面的书:
- 入门优先:从《Spring Security实战》或《Spring Boot Up and Running》开始,快速上手。
- 深入协议:先读《OAuth 2 in Action》,再结合《Pro Spring Security》实践。
- 全面掌握:《Spring Security in Action》是当前最系统的选择,覆盖现代安全场景(如微服务、响应式)