第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 {@Beanpublic 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 {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic 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;@Overridepublic 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;}@GetMappingpublic 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 {@Overridepublic 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》是当前最系统的选择,覆盖现代安全场景(如微服务、响应式)