【SpringSecurity】快速入门—通俗易懂

1.导入依赖

首先,在pom.xml文件中添加Spring Security依赖:

代码语言:javascript
复制
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

2.继承WebSecurityConfigurerAdapter

创建一个WebSecurityConfig类,继承WebSecurityConfigurerAdapter:

继承WebSecurityConfigurerAdapter是因为我们要重写里面的方法 定义一些安全配置 例如:注销

功能、访问控制、登录页面等。

代码语言:javascript
复制
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

}

protected void configure(HttpSecurity http)这个方法用于配置拦截保护的HTTP请求 配置HTTP安

全性 定义哪些URL应该受到保护 哪些用户可以访问哪些URL 以及保护的URL应该执行哪些安全措

代码语言:javascript
复制
	@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
//.loginPage("/login.html")//指定登录页面(这里不会前端,所以不写页面了)
//.loginProcessingUrl("/authentication/form")//登录访问路径(登录页面的form表单提交路径)
//.failureUrl("/error.html")//用户名或密码错误访问的页面
//.usernameParameter("username")
//.passwordParameter("password") //要认证的 用户名, 密码 的参数名,默认username, password
.defaultSuccessUrl("/index/toIndex").permitAll()//defaultSuccessUrl:登录成功后,是否始终跳转到登录成功url,它默认为false。 permitAll()就是指不需要拦截
.and().authorizeRequests()
.antMatchers("/index/testRoleAndPermission").hasAnyRole("admin")//设置哪些路径需要什么权限
.antMatchers("/index/anyOne","/index/anyTwo").permitAll()//设置哪些路径可以直接访问,不需要认证
.anyRequest().authenticated();//其余任何请求都需要认证
.and().csrf()
.disable();//关闭csrf防护
}

public void configure(AuthenticationManagerBuilder auth) 这个方法主要是配置 身份验证机制 就

是配置密码匹配器和用户从数据库查询用户的service

代码语言:javascript
复制
// 用于配置身份验证机制
// AuthenticationManagerBuilder常用方法:(下面的方法我没有写参数,实际情况可能是有参的)
// inMemoryAuthentication(): 这个方法允许您在内存中配置用户详细信息。您可以指定用户名、密码和用户角色。这对于快速测试和开发目的非常方便。
// userDetailsService(): 这个方法接受一个UserDetailsService对象,用于加载用户的详细信息。UserDetailsService是一个接口,您需要实现它来根据用户名加载用户信息。
// jdbcAuthentication(): 这个方法允许您使用JDBC来加载用户详细信息。您需要提供一个DataSource对象和相应的查询语句来检索用户名、密码和角色信息。
// ldapAuthentication(): 这个方法用于配置LDAP(轻量级目录访问协议)身份验证。您需要提供LDAP服务器的连接信息和相应的查询语句。
// authenticationProvider(): 这个方法允许您提供自定义的AuthenticationProvider实现,用于验证用户的身份。

	// userDetailsService(T userDetailsService):    根据传入的自定义UserDetailsService做身份验证。
	// 返回值:                                       会返回一个DaoAuthenticationConfigurer(该类继承AbstractDaoAuthenticationConfigurer抽象类)}
    // passwordEncoder(PasswordEncoder passwordEncoder): 来自于抽象类, 用于指定自定义密码匹配器,便于后续springsecutiry底层后续使用。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}</code></pre></div></div><p>public PasswordEncoder passwordEncoder() 这个是密码匹配器 SpringSecurity校验密码的时候</p><p>就要用到密码匹配器 对密码进行加密解密校验对比操作</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">// 必须要个这种密码匹配器,springsecurity要求的,必须对密码进行加密。
@Bean
public PasswordEncoder passwordEncoder(){
	return new BCryptPasswordEncoder();
}</code></pre></div></div><h3 id="qv4s" name="3.%E5%AE%9E%E7%8E%B0UserDetailsService">3.实现UserDetailsService</h3><p>客户端输入用户名和密码 后端必须要从数据库查询出数据 进行校验 需要实现UserDetailsService</p><p>重写查询校验方法</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">/**
  • @author lihao
  • @version 1.0
  • @ClassName SecurityUserServiceImpl
  • @date 2023/5/25 9:43
  • @apiNote UserDetailsService:用于加载用户的详细信息,以供身份验证和授权使用。它提供了一个方法loadUserByUsername(String username)用于用户名获取用户的详细信息。
  •                          我们想更改springcurity的认证,我们就可以实现UserDetailsService,因为security底层就是用其做核心认证的。
    

**/
@Service
public class SecurityUserServiceImpl extends ServiceImpl<SecurityUserMapper, SecurityUser> implements SecurityUserService, UserDetailsService {

@Resource
private SecurityUserMapper securityUserMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
	LambdaQueryWrapper&lt;SecurityUser&gt; wrapper = new LambdaQueryWrapper&lt;&gt;();
	wrapper.eq(SecurityUser::getUsername,username);
	SecurityUser user = securityUserMapper.selectOne(wrapper);
	if (user==null) {
		throw new UsernameNotFoundException(&#34;用户没找到&#34;);
	}
	
    // loadUserByUsername方法需要返回一个UserDetails(接口),常用实现类User。因此我们返回一个SpringSecurity提供的User对象。
    // 该User对象所需的参数中,密码必须加密(由springsecurity要求),因为我前面已经在SecurityConfig配置了加密器,所以这里就不需要对密码进行加密。
    // 否则需要加密:new BCryptPasswordEncoder().encode(user.getPassword())  ---- 可以这么写

	// 第三个参数,他需要一个权限集合,这里只做认证,所以随便搞一个角色集合,不影响认证,注意,这里第三个参数不能直接传null,必须给它一个集合。
	List&lt;GrantedAuthority&gt; adminAuthority = AuthorityUtils.commaSeparatedStringToAuthorityList(&#34;admin&#34;);
	return new User(user.getUsername(), user.getPassword(), adminAuthority); // 已在SecurityConfig配置了加密组件,所以不需要对这里的密码加密。
}</code></pre></div></div><h3 id="9b0q6" name="4.%E8%AE%B0%E4%BD%8F%E6%88%91">4.记住我</h3><p>客户端登录一次以后 下次访问不用登录 直接访问 </p><p>需要在configure(HttpSecurity http)配置</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">// 开启记住我功能

http.rememberMe()
.tokenRepository(tokenRepository)
.userDetailsService(usersService);

.tokenRepository(tokenRepository):这是为"记住我"功能配置token存储库。token是用来存储

和验证用户会话信息的。这通常是一个在数据库或其他持久性存储中保存信息的对象。

.userDetailsService(usersService):这是为"记住我"功能配置用户详情服务。

UserDetailsService是Spring Security中的一个接口,它有一个方法loadUserByUsername,用于根

据用户名获取用户信息。usersService需要实现这个接口,并能够根据用户名找到用户的详细信

息。这对于"记住我"功能很重要,因为它需要知道用户的详细信息(例如他们的加密密码)以验证

他们的身份。

代码语言:javascript
复制
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new
JdbcTokenRepositoryImpl();
// 赋值数据源
jdbcTokenRepository.setDataSource(dataSource);
// 自动创建表,第一次执行会创建,以后要执行就要删除掉!
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}

还可以设置有效期

5.用户注销

代码语言:javascript
复制
http.logout().logoutUrl("/logout").logoutSuccessUrl("/index").permitAll
();
  1. http.logout(): 这是调用Spring Security的HTTP安全性配置的logout方法,它配置了用户的注销功能。
  2. .logoutUrl("/logout"): 这告诉Spring Security,当用户点击注销时,应该将他们重定向到URL "/logout"。这通常是应用程序的一个特殊页面,它执行注销操作并终止用户的会话。
  3. .logoutSuccessUrl("/index"): 当注销操作成功后,用户将被重定向到这个URL。在这个例子中,用户将被重定向到应用程序的"/index"页面。
  4. .permitAll(): 这告诉Spring Security,所有用户都应该能够访问注销功能。换句话说,它不限制谁可以注销,所有用户都可以。

总的来说,这段代码的目的是配置Spring Security的注销功能,使得所有用户都可以注销,并且当

他们注销成功后,他们将被重定向到应用程序的"/index"页面。

6.CSRF理解

跨站请求伪造 (英语: Cross-site request forgery ),也被称为 one-click

attack 或者 session riding ,通常缩写为 CSRF 或者 XSRF , 是一种挟制用户在当前已

登录的 Web 应用程序上执行非本意的操作的攻击方法。跟 跨网站脚本 ( XSS )相比, XSS

利用的是用户对指定网站的信任, CSRF 利用的是网站对用户网页浏览器的信任。

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个

自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买

商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。

这利用了 web 中用户身份验证的一个漏洞: 简单的身份验证只能保证请求发自某个用户的

浏览器,却不能保证请求本身是用户自愿发出的

从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用

程序, Spring Security CSRF 会针对 PATCH , POST , PUT 和 DELETE 方法进行防护。

开启CSRF后,Spring Security会添加一个CSRF令牌到表单提交的请求中,以确保只有合法的请

求才能被处理。这样可以防止攻击者通过注入恶意代码或跨站请求伪造等方式来篡改服务器资源。

7.注解功能

使用注解功能前 要先开启注解功能 在启动类加上@EnableGlobalMethodSecurity()注解 注解里面

要使用什么注解 就在括号里填写 xxx注解=true 例如:

@EnableGlobalMethodService(securedEnabled = true) 使用@securedEnabled注解

代码语言:javascript
复制
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled=true)
public class DemosecurityApplication {
public static void main(String[] args) {
SpringApplication.run(DemosecurityApplication.class, args);
}
}

7.1@Secured

判断是否具有这个角色 字符串前缀需要添加ROLE_

代码语言:javascript
复制
// 测试注解:
@RequestMapping("testSecured")
@ResponseBody
@Secured({"ROLE_normal","ROLE_admin"})
public String helloUser() {
return "hello,user";
}

7.2@PreAuthorized 

先开启注解功能:

@EnableGlobalMethodSecurity (prePostEnabled = true )

@PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用

户的 roles/permissions 参数传到方法中。

代码语言:javascript
复制
@RequestMapping("/preAuthorize")
@ResponseBody
//@PreAuthorize("hasRole('ROLE_管理员')")
@PreAuthorize("hasAnyAuthority('menu:system')")
public String preAuthorize(){
System.out.println("preAuthorize");
return "preAuthorize";
}

7.3@PostAuthorized

先开启注解功能:

@EnableGlobalMethodSecurity (prePostEnabled = true )

@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值

的权限 .

代码语言:javascript
复制
@RequestMapping("/testPostAuthorize")
@ResponseBody
@PostAuthorize("hasAnyAuthority('menu:system')")
public String preAuthorize(){
System.out.println("test--PostAuthorize");
return "PostAuthorize";
}

7.4@PostFilter

@PostFilter :权限验证之后对数据进行过滤 留下用户名是 admin1 的数据

表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素

代码语言:javascript
复制
@RequestMapping("getAll")
@PreAuthorize("hasRole('ROLE_管理员')")
@PostFilter("filterObject.username == 'admin1'")
@ResponseBody
public List<UserInfo> getAllUser(){
ArrayList<UserInfo> list = new ArrayList<>();
list.add(new UserInfo(1l,"admin1","6666"));
list.add(new UserInfo(2l,"admin2","888"));
return list;
}

7.5Z@PreFilter

@PreFilter: 进入控制器之前对数据进行过滤

代码语言:javascript
复制
@RequestMapping("getTestPreFilter")
@PreAuthorize("hasRole('ROLE_管理员')")
@PreFilter(value = "filterObject.id%2==0")
@ResponseBody
public List<UserInfo> getTestPreFilter(@RequestBody List<UserInfo>
list){
list.forEach(t-> {
System.out.println(t.getId()+"\t"+t.getUsername());
});
return list;
}

8.原理解析

SpringSecurity本质:其实就是一个过滤器链,内部提供了各种功能的过滤器。springsecurity底层就是这些过滤器一层一层的执行帮我们实现的权限管理。

图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。

UsernamePasswordAuthenticationFilter: 用于处理基于表单的登录请求,从表单中获取用户名和

密码。 默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name

值为 username 和 password。 这两个值可以通过设置这个过滤器的usernameParameter 和

passwordParameter 两个参数的值进行修改。 其内部还有 登录成功 或 失败后 进行处理的

AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改

变。

ExceptionTranslationFilter: 处理 AccessDeniedException 和 AuthenticationException 异常。 3.

FilterSecurityInterceptor: 是用于保护web资源的,使用 AccessDecisionManager 对当前用户进

行授权访问。

SpringSecurity认证流程: