Spring Security 笔记 (1.6w字讲解)
Spring Security 基础
1、开发环境配置
2、认证
3、其他配置
4、授权
*5、内部机制探究
一、开发环境配置
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>6.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>6.1.1</version>
</dependency>初始化器:
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
//不用重写任何内容
//这里实际上会自动注册一个Filter,SpringSecurity底层就是依靠N个过滤器实现的
}配置类,添加@EnableWebSecurity注解开启WebSecurity相关功能:
@Configuration
@EnableWebSecurity //开启WebSecurity相关功能
public class SecurityConfiguration {
}在根容器中添加配置文件:
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{MainConfiguration.class, SecurityConfiguration.class};
}
}二、认证
基于内存验证
只需要在Security配置类中注册一个Bean即可:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean //UserDetailsService就是获取用户信息的服务
public UserDetailsService userDetailsService() {
//每一个UserDetails就代表一个用户信息,其中包含用户的用户名和密码以及角色
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER") //角色目前我们不需要关心,随便写就行,后面会专门讲解
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
//创建一个基于内存的用户信息管理器作为UserDetailsService
}
}但是上面代码中,明文密码容易泄露。
通过将用户的密码进行Hash值计算,计算出来的结果一般是单向的,无法还原为原文,如果需要验证是否与此密码一致,那么需要以同样的方式加密再比较两个Hash值是否一致,这样就很好的保证了用户密码的安全性。
我们在配置用户信息的时候,可以使用官方提供的BCrypt加密工具:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
//这里将BCryptPasswordEncoder直接注册为Bean,Security会自动进行选择
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails user = User
.withUsername("user")
.password(encoder.encode("password")) //这里将密码进行加密后存储
.roles("USER")
.build();
System.out.println(encoder.encode("password")); //一会观察一下加密出来之后的密码长啥样
UserDetails admin = User
.withUsername("admin")
.password(encoder.encode("password")) //这里将密码进行加密后存储
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}注:上面的参数一般写在application.yml配置中
csrf防护
SpringSecurity自带了csrf防护,需求我们在POST请求中携带页面中的csrfToken才可以,否则一律进行拦截操作。
axios
在页面中嵌入:
<input type="text" th:id="${_csrf.getParameterName()}" th:value="${_csrf.token}" hidden>发送请求时携带_csrf
function pay() {
const account = document.getElementById("account").value
const csrf = document.getElementById("_csrf").value
axios.post('/mvc/pay', {
account: account,
_csrf: csrf //携带此信息即可,否则会被拦截
}, {
...form
<form action="/xxxx" method="post">
...
<input type="text" th:name="${_csrf.getParameterName()}" th:value="${_csrf.token}" hidden>
...
</form>注:现在的浏览器已经很安全了,完全不需要使用自带的csrf防护。
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
...
//以下是csrf相关配置
.csrf(conf -> {
conf.disable(); //此方法可以直接关闭全部的csrf校验,一步到位
conf.ignoringRequestMatchers("/xxx/**"); //此方法可以根据情况忽略某些地址的csrf校验
})
.build();
}
}基于数据库验证
官方默认提供了可以直接使用的用户和权限表设计,直接使用即可:
create table users(username varchar(50) not null primary key,password varchar(500) not null,enabled boolean not null);
create table authorities (username varchar(50) not null,authority varchar(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public DataSource dataSource(){
//数据源配置
return new PooledDataSource("com.mysql.cj.jdbc.Driver",
"jdbc:mysql://localhost:3306/test", "root", "123456");
}
@Bean
public UserDetailsService userDetailsService(DataSource dataSource,
PasswordEncoder encoder) {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
//仅首次启动时创建一个新的用户用于测试,后续无需创建
manager.createUser(User.withUsername("user")
.password(encoder.encode("password")).roles("USER").build());
return manager;
}
}基于内存的InMemoryUserDetailsManager和基于数据库的JdbcUserDetailsManager,都是实现自UserDetailsManager接口。
// 源码:
public interface UserDetailsManager extends UserDetailsService {
//创建一个新的用户
void createUser(UserDetails user);
//更新用户信息
void updateUser(UserDetails user);
//删除用户
void deleteUser(String username);
//修改用户密码
void changePassword(String oldPassword, String newPassword);
//判断是否存在指定用户
boolean userExists(String username);
}在SecurityConfiguration中,提供JdbcUserDetailsManager,并设置AuthenticationManager:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
...
//手动创建一个AuthenticationManager用于处理密码校验
private AuthenticationManager authenticationManager(UserDetailsManager manager,
PasswordEncoder encoder){
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(manager);
provider.setPasswordEncoder(encoder);
return new ProviderManager(provider);
}
@Bean
public UserDetailsManager userDetailsService(DataSource dataSource,
PasswordEncoder encoder) throws Exception {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
//为UserDetailsManager设置AuthenticationManager即可开启重置密码的时的校验
manager.setAuthenticationManager(authenticationManager(manager, encoder));
return manager;
}
}接口:
@ResponseBody
@PostMapping("/change-password")
public JSONObject changePassword(@RequestParam String oldPassword,
@RequestParam String newPassword) {
manager.changePassword(oldPassword, encoder.encode(newPassword));
JSONObject object = new JSONObject();
object.put("success", true);
return object;
}前端:
<div>
<label>
修改密码:
<input type="text" id="oldPassword" placeholder="旧密码"/>
<input type="text" id="newPassword" placeholder="新密码"/>
</label>
<button onclick="change()">修改密码</button>
</div>function change() {
const oldPassword = document.getElementById("oldPassword").value
const newPassword = document.getElementById("newPassword").value
const csrf = document.getElementById("_csrf").value
axios.post('/mvc/change-password', {
oldPassword: oldPassword,
newPassword: newPassword,
_csrf: csrf
}, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).then(({data}) => {
alert(data.success ? "密码修改成功" : "密码修改失败,请检查原密码是否正确")
})
}自定义验证
自行实现UserDetailsService或是功能更完善的UserDetailsManager接口,这里我们选择前者进行实现:
@Service
public class AuthorizeService implements UserDetailsService {
@Resource
UserMapper mapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = mapper.findUserByName(username);
if(account == null)
throw new UsernameNotFoundException("用户名或密码错误");
return User
.withUsername(username)
.password(account.getPassword())
.build();
}
}完整认证流程是这样的:
用户提交 username + password
Spring Security 调用 UserDetailsService.loadUserByUsername(username)
返回一个 UserDetails,其中包含:
- username
- password(数据库中存的加密密码)
接下来由 AuthenticationProvider(实现类DaoAuthenticationProvider) 做密码校验:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
// 这里注入 UserDetailsService
private UserDetailsService userDetailsService;
// 这里注入 PasswordEncoder
private PasswordEncoder passwordEncoder;
@Override
protected void additionalAuthenticationChecks(
UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) {
// 获取用户输入的密码
String presentedPassword = authentication.getCredentials().toString();
// 自动进行密码比对 ← 这是核心!
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
throw new BadCredentialsException("密码错误");
}
}
}三、其他配置
自定义登录界面
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
...
//新版本全部采用lambda形式进行配置,无法再使用之前的and()方法进行连接了
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
//以下是验证请求拦截和放行配置
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/static/**").permitAll(); //将所有的静态资源放行,一定要添加在全部请求拦截之前
auth.anyRequest().authenticated(); //将所有请求全部拦截,一律需要验证
})
//以下是表单登录相关配置
.formLogin(conf -> {
conf.loginPage("/login"); //将登录页设置为我们自己的登录页面
conf.loginProcessingUrl("/doLogin"); //登录表单提交的地址,可以自定义
conf.defaultSuccessUrl("/"); //登录成功后跳转的页面
conf.permitAll(); //将登录相关的地址放行,否则未登录的用户连登录界面都进不去
//用户名和密码的表单字段名称,不过默认就是这个,可以不配置,除非有特殊需求
conf.usernameParameter("username");
conf.passwordParameter("password");
})
//以下是退出登录相关配置
.logout(conf -> {
conf.logoutUrl("/doLogout"); //退出登录地址,跟上面一样可自定义
conf.logoutSuccessUrl("/login"); //退出登录后跳转的地址,这里设为登录界面
conf.permitAll();
})
.build();
}
}登录表单:
<form action="doLogin" method="post">
...
<input type="text" name="username" placeholder="Email Address" class="ad-input">
...
<input type="password" name="password" placeholder="Password" class="ad-input">
...
<input type="text" th:name="${_csrf.getParameterName()}" th:value="${_csrf.token}" hidden>
<div class="ad-auth-btn">
<button type="submit" class="ad-btn ad-login-member">Login</button>
</div>
...
</form>退出登录按钮:
<li>
<form action="doLogout" method="post">
<input type="text" th:name="${_csrf.getParameterName()}" th:value="${_csrf.token}" hidden>
<button type="submit">
<i class="fas fa-sign-out-alt"></i> logout
</button>
</form>
</li>记住我功能
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
...
.rememberMe(conf -> {
conf.alwaysRemember(false); //这里不要开启始终记住,我们需要配置为用户自行勾选
conf.rememberMeParameter("remember-me"); //记住我表单字段,默认就是这个,可以不配置
conf.rememberMeCookieName("xxxx"); //记住我设置的Cookie名字,也可以自定义,不过没必要
})
.build();
}
}<div class="ad-checkbox">
<label>
<input type="checkbox" name="remember-me" class="ad-checkbox">
<span>Remember Me</span>
</label>
</div>这个Cookie信息的过期时间并不是仅会话,而是默认保存一段时间,因此,我们关闭浏览器后下次再次访问网站时,就不需要我们再次进行登录操作了,而是直接继续上一次的登录状态。
由于记住我信息是存放在内存中的,我们需要保证服务器一直处于运行状态,如果关闭服务器的话,记住我信息会全部丢失,因此,如果我们希望记住我能够一直持久化保存,就需要进一步进行配置。
我们需要创建一个基于JDBC的TokenRepository实现:
@Bean
public PersistentTokenRepository tokenRepository(DataSource dataSource){
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
//在启动时自动在数据库中创建存储记住我信息的表,仅第一次需要,后续不需要
repository.setCreateTableOnStartup(true);
repository.setDataSource(dataSource);
return repository;
}.rememberMe(conf -> {
conf.rememberMeParameter("remember-me");
conf.tokenRepository(repository); //设置刚刚的记住我持久化存储库
conf.tokenValiditySeconds(3600 * 7); //设置记住我有效时间为7天
})四、授权
SpringSecurity为我们提供了两种授权方式:
- 基于权限的授权:只要拥有某权限的用户,就可以访问某个路径。
- 基于角色的授权:根据用户属于哪个角色来决定是否可以访问某个路径。
.authorizeHttpRequests(auth -> {
//静态资源依然全部可以访问
auth.requestMatchers("/static/**").permitAll();
//只有具有以下角色的用户才能访问路径"/"
auth.requestMatchers("/").hasAnyRole("user", "admin");
//其他所有路径必须角色为admin才能访问
auth.anyRequest().hasRole("admin");
}) @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = mapper.findUserByName(username);
if(account == null)
throw new UsernameNotFoundException("用户名或密码错误");
return User
.withUsername(username)
.password(account.getPassword())
.roles(account.getRole()) //添加角色,一个用户可以有一个或多个角色
.build();
}基于权限授权
.authorizeHttpRequests(auth -> {
//静态资源依然全部可以访问
auth.requestMatchers("/static/**").permitAll();
//基于权限和基于角色其实差别并不大,使用方式是相同的
auth.anyRequest().hasAnyAuthority("page:index");
})使用注解权限判断
@EnableMethodSecurity 可以写在任何 @Configuration 类上。
只要这个配置类被 Spring 扫描到即可。
@Configuration
@EnableWebSecurity
@EnableMethodSecurity //开启方法安全校验
public class SecurityConfiguration {
...
}@Controller
public class HelloController {
@PreAuthorize("hasRole('user')") //直接使用hasRole方法判断是否包含某个角色
@GetMapping("/")
public String index(){
return "index";
}
...
}@Service
public class UserService {
@PreAuthorize("hasAnyRole('user')")
public void test(){
System.out.println("成功执行");
}
}与具有相同功能的还有@Secured,但是它不支持SpEL表达式的权限表示形式,并且需要添加"ROLE_"前缀。
@PreFilter和@PostFilter
我们还可以使用@PreFilter和@PostFilter对集合类型的参数或返回值进行过滤。
@PreFilter("filterObject.equals('lbwnb')") //filterObject代表集合中每个元素,只要满足条件的元素才会留下
public void test(List<String> list){
System.out.println("成功执行"+list);
}注:当有多个集合时,需要使用filterTarget进行指定:
@PreFilter(value = "filterObject.equals('lbwnb')", filterTarget = "list2")
public void test(List<String> list, List<String> list2){
System.out.println("成功执行"+list);
} 本文系作者 @xiin 原创发布在To Future$站点。未经许可,禁止转载。
暂无评论数据