文章目录
参考牛客网高级项目教程
尚硅谷SpringSecurity教程笔记
一、认识SpringSecurity
1. 简介
-
Spring Security 是 Spring 家族中的成员。
- Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方
案。 - 可扩展,以满足自定义的需求
- Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方
-
Spring Security 重要核心功能, 也是Web 应用的安全性
-
用户认证( Authentication)
-
用户授权( Authorization)
-
1)用户认证指的是:
- 验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。
- 用户认证一般要求用户提供用户名和密码。
- 还可以通过其他方式认证,比如验证码、短信等
-
2)用户授权指的是
-
验证某个用户是否有权限执行某个操作。
-
在一个系统中,不同用户所具有的权限是不同的。
-
比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。
-
一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
-
通俗点讲就是系统判断用户是否有权限去做某些事情。
-
-
2. 历史
在早些时候,这个项目没有任何自己的验证模块,身份验证过程依赖于容器管理的安全性和 Acegi 安全性。而不是专注于授权。开始的时候这很适合,但是越来越多的用户请求额外的容器支持。容器特定的认证领域接口的基本限制变得清晰。还有一个相关的问题增加新的容器的路径,这是最终用户的困惑和错误配置的常见问题。
Acegi 安全特定的认证服务介绍。大约一年后, Acegi 安全正式成为了 Spring 框架的子项目。 1.0.0 最终版本是出版于 2006 -在超过两年半的大量生产的软件项目和数以百计的改进和积极利用社区的贡献。
Acegi 安全 2007 年底正式成为了 Spring 组合项目,更名为"Spring Security"
3. 特点
-
1.全面的权限控制,提供可扩展的支持。
-
2.可以防止各种网络攻击
-
3。专门为 Web 开发而设计,支持与Servlet API、Spring MVC等WEb框架技术集成。
- 旧版本不能脱离 Web 环境使用。
- 新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独引入核心模块就可以脱离 Web 环境。
-
重量级。
与Shiro的比较
4. 底层原理
-
SpringSecurity基于Filter拦截器进行相关验证授权操作
- 因此,判断时机很靠前
-
代码底层流程:重点看三个过滤器:
FilterSecurityInterceptor
:
- 是一个方法级的权限过滤器, 基本位于过滤链的最底部。
ExceptionTranslationFilter
:
- 是个异常过滤器,用来处理在认证授权过程中抛出的异常
UsernamePasswordAuthenticationFilter
:
- 对/login 的 POST 请求做拦截,校验表单中用户名,密码。
二、SpringSecurity演示示例
1. 导包
- 导入包后,整个项目就会立即起作用-起到拦截效果
<!-- SpringSecurity 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. 用户权限的设定
2.1 授权管理相关概念
2.2 自定义授权验证逻辑
UserDetailsService
-
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。
-
而在实际项目中账号和密码都是从数据库中查询出来的。
-
所以要在业务层通过自定义逻辑控制认证逻辑。
-
如果需要自定义逻辑时,需要实现 UserDetailsService 接口即可。接口定义如下:
-
-
因此,在业务层Service继承这个接口,重写方法,根据用户名查询到指定用户,系统自动进行认证授权
@Service public class UserService implements UserDetailsService { @Autowired private UserMapper userMapper; public User findUserByName(String username) { return userMapper.selectByName(username); } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return findUserByName(username); } }
2.3 处理User主体类
- 先对用户User实体类进行权限设定,根据Type类型来设定,用字符串表示
- 不同于数据库的用户表、角色表、权限表的多表查询,
- 网页授权相对简单,本例以用户的type属性来确定角色,一般一个用户就一个角色,再根据角色赋予相应权限
UserDetails
-
主体要继承这个接口,这个接口中声明了对主体认证的几个基本方法
getAuthorities()
- 获取一组授权形式
- 根据type字段属性设置权限字符串表现形式
- 这样,只要查询到指定用户,就会获取到这个用户的访问权限,是普通用户还是管理员
GrantedAuthority
- 创建授权权限的实现类
// true:账号未过期
@Override
public boolean isAccountNonExpired() {
return true;
}
// true:账号未锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
// true:凭证(密码)未过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// true:账号可用
@Override
public boolean isEnabled() {
return true;
}
// 设置权限字符串表现形式
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> list = new ArrayList<>();
list.add(new GrantedAuthority() {
@Override
public String getAuthority() {
switch (type) {
case 1:
return "ADMIN"; // 管理员
default:
return "USER"; // 普通用户
}
}
});
return list;
}
3. 配置文件-认证权限自定义方案
- 即配置拦截器,用以自定义拦截规则
WebSecurityConfigurerAdapter
- 适配器模式在spring中被广泛的使用,
- 在配置中使用Adapter的好处便是,我们可以选择性的配置想要修改的那一部分配置,而不用覆盖其他不相关的配置。
- WebSecurityConfigurerAdapter中我们可以选择自己想要修改的内容,来进行重写,而其提供了三个configure重载方法,是我们主要关心的
AuthenticationManagerBuilder
,WebSecurity
,HttpSecurity
1. WebSecurity
- 配置拦截的路径,可以指定忽略静态资源的访问,提高性能
@Override
public void configure(WebSecurity web) throws Exception {
// 忽略静态资源的访问
web
.ignoring()
.antMatchers("/resources/**");
}
2. AuthenticationManagerBuilder
- AuthenticationManagerBuilder:用于构建AuthenticationManager对象的构造器
- 想要在WebSecurityConfigurerAdapter中==进行认证相关的配置,可以使用configure(AuthenticationManagerBuilder auth)暴露一个AuthenticationManager的建造器==
AuthenticationManager
- 认证的核心接口
ProviderManager
- AuthenticationManager接口的默认实现类
authenticationProvider
-
ProviderManager持有的一组authenticationProvider,每个authenticationProvider负责一种认证
-
委托模式:ProviderManager将认证委托给了authenticationProvider
-
重写两个方法:
- authenticate:认证逻辑
- supports:认证的类型
// 自定义认证规则 // authenticationProvider:ProviderManager持有的一组authenticationProvider, // 每个authenticationProvider负责一种认证 // 委托模式:ProviderManager将认证委托给了authenticationProvider auth.authenticationProvider(new AuthenticationProvider() { // Authentication:用于封装认证信息的接口,不同的实现类代表不同类型的认证信息 @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { } // 当前的authenticationProvider支持哪种类型的认证 @Override public boolean supports(Class<?> aClass) { // UsernamePasswordAuthenticationToken:authenticate接口的常用实现类 return UsernamePasswordAuthenticationToken.class.equals(aClass); } });
Authentication
-具体类型的认证接口
- 用于封装认证信息的接口,不同的实现类代表不同类型的认证信息
getName()
:获取认证名
- 账号密码类型必须是username
getCredentials()
:获取认证凭证
- 账号密码类型必须是password
new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities())
-
返回的指定认证类型
- 主体:继承UserDetails接口的实体类
- 凭证:本类型中为密码
- -权限:在实体类中定义的权限-根据type属性设定的权限
supports(Class<?> aClass)
-当前authenticationProvider
支持的类型
-
authenticationProvider
接口要重新的方法,用以指定这个接口要认证的类型 -
UsernamePasswordAuthenticationToken
,表明此次指定的认证类型是账号密码类型// 当前的authenticationProvider支持哪种类型的认证 @Override public boolean supports(Class<?> aClass) { // UsernamePasswordAuthenticationToken:authenticate接口的常用实现类 return UsernamePasswordAuthenticationToken.class.equals(aClass); }
-
整个认证逻辑代码如下
@Autowired
private UserService userService;
/**
* AuthenticationManager:认证的核心接口
* ProviderManager: AuthenticationManager接口的默认实现类.
* @param auth AuthenticationManagerBuilder:用于构建AuthenticationManager对象的工具
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 内置认证规则
//auth.userDetailsService(userService).passwordEncoder(new Pbkdf2PasswordEncoder("12345"));
// 自定义认证规则
// authenticationProvider:ProviderManager持有的一组authenticationProvider,
// 每个authenticationProvider负责一种认证
auth.authenticationProvider(new AuthenticationProvider() {
// Authentication:用于封装认证信息的接口,不同的实现类代表不同类型的认证信息
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
// 自定义的账号密码验证规则
User user = userService.findUserByName(username);
if(user == null) {
throw new UsernameNotFoundException("账号不存在!");
}
password = CommunityUtil.md5(password + user.getSalt());
if(!user.getPassword().equals(password)) {
throw new BadCredentialsException("密码不正确!");
}
// principal:主要信息-主体; credentials: 证书-密码; authorities: 权限;
return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
}
// 当前的authenticationProvider支持哪种类型的认证
@Override
public boolean supports(Class<?> aClass) {
// UsernamePasswordAuthenticationToken:authenticate接口的常用实现类
return UsernamePasswordAuthenticationToken.class.equals(aClass);
}
});
}
3.HttpSecurity
官网示例:
-
其中http作为根开始配置,每一个and()对应了一个模块的配置(等同于xml配置中的结束标签),
-
并且and()返回了HttpSecurity本身,于是可以连续进行配置。他们配置的含义也非常容易通过变量本身来推测,
- authorizeRequests()配置路径拦截,表明路径访问所对应的权限,角色,认证信息。
- formLogin()对应表单认证相关的配置
- logout()对应了注销相关的配置
- httpBasic()可以配置basic登录
- etc
-
他们分别代表了http请求相关的安全配置**,这些配置项无一例外的返回了Configurer类**,而所有的http相关配置可以通过查看HttpSecurity的主要方法得知:
http.formLogin()
-登录权限
-
注意重定向与转发的区别
- 重定向-A与B没有耦合,A中数据无法带给B
- 是两次请求,只是A建议浏览器访问B
- 转发-A与B有耦合,A中数据可以转发给B,返回给浏览器
- 只有一次请求,浏览器只认识A,不认识B
http.logout()
-退出处理
http.authorizeRequests()
-授权配置
// 授权配置
http.authorizeRequests()
.antMatchers("/letter").hasAnyAuthority("USER", "ADMIN")// 指定路径的访问权限-登录用户和管理员才能访问
.antMatchers("/admin").hasAnyAuthority("ADMIN") // 只有管理员才能访问指定的访问路径
.and().exceptionHandling().accessDeniedPage("/denied");
http.addFilterBefore(new Filter()
-增加验证码过滤器
http.rememberMe()
-记住我配置
- 整体代码展示
// 相关权限配置
@Override
protected void configure(HttpSecurity http) throws Exception {
//父类已经配置好了权限相关配置,若想自定义,需要重写方法进行覆盖
//uper.configure(http);
// 登录相关配置
http.formLogin()
.loginPage("/loginpage") // 显示登录表单页面的请求路径
.loginProcessingUrl("/login") // 表单中通过post请求提交数据的请求路径-拦截验证数据
.successHandler(new AuthenticationSuccessHandler() { // 验证成功后的处理逻辑
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 成功了,采用重定向的返回方式,因为首页与登录页面没有耦合-建议浏览器访问主页请求
response.sendRedirect(request.getContextPath() + "/index");
}
})
.failureHandler(new AuthenticationFailureHandler() { // 验证失败后的处理逻辑
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
// 失败了,要返回登录表单,采用转发的方式,将之前填入的数据转绑定在request中,在表单中显示出来,避免客户再次输入
// 不能使用重定向,因为重定向会再次返回request,会传入新的数据,
// 直接返回模板页面也行,但此方法不支持
request.setAttribute("error", e.getMessage());
request.getRequestDispatcher("/loginpage").forward(request, response);
}
});
// 退出相关配置
http.logout()
.logoutUrl("/logout") // 退出请求
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 退出后,重定向到首页
response.sendRedirect(request.getContextPath() + "/index");
}
});
// 授权配置
http.authorizeRequests()
.antMatchers("/letter").hasAnyAuthority("USER", "ADMIN")// 指定路径的访问权限-登录用户和管理员才能访问
.antMatchers("/admin").hasAnyAuthority("ADMIN") // 只有管理员才能访问指定的访问路径
.and().exceptionHandling().accessDeniedPage("/denied");
// 增加Filter,处理验证码
http.addFilterBefore(new Filter() { // 在某个拦截器之前加上这个拦截器-第二个参数会指定拦截器类型
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 只处理登录的请求
if(request.getServletPath().equals("/login")) {
String verifyCode = request.getParameter("verifyCode"); // 获取用户输入的验证码
if(verifyCode == null || !verifyCode.equalsIgnoreCase("1234")) {
request.setAttribute("error", "验证码错误!");
request.getRequestDispatcher("/loginpage").forward(request, response);
return;
}
}
// 让请求继续向下执行
filterChain.doFilter(request, response);
}
}, UsernamePasswordAuthenticationFilter.class); // 第二个参数表示在哪个filter之前加上新的拦截器
// 记住我
http.rememberMe()
.tokenRepository(new InMemoryTokenRepositoryImpl()) // 保存在内存中
.tokenValiditySeconds(3600 * 24) // 有效时间
.userDetailsService(userService); // 在内存中通过主体用户名查询到主体信息,自动通过验证保存登录状态
}
4. controller层处理请求
- 认证成功后,结果会通过SecurityContextHolder存入SecurityContext中(底层实现)
- 可以获取认证的主体User
5. 模板页面处理
登录页面
- 账号和密码标签命名必须是username, password,SpingSecurity才能识别并传入用户输入的值
- 错误时,返回要带有之前输入的值,value传入用户之前输入的参数的值
主页模板
- 框架要求退出必须是post请求
- 通过js提交,获取当前页面中所有表单中的第一个表单进行提交