安全框架对比
Shiro
易于理解的 Java Security API;
简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等);
对角色的简单的鉴权(访问控制),支持细粒度的鉴权;
支持一级缓存,以提升应用程序的性能;
内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境; 异构客户端会话访问;
非常简单的加密 API;
不跟任何的框架或者容器捆绑,可以独立运行。
SpringSecurity
- 拥有Shiro的所有功能
- 拥有对Oauth,OpenID的支持
- 权限细粒度更高
OpenID是Authentication,是网站对用户进行认证,让网站知道“你是你所声称的URL的属主”,例如使用身份证可以证明自己的身份.
OAuth是Authorization,其实并不包括认证,只不过“只有认证成功的人才能进行授权”,例如某些网站可以直接使用QQ,微信免注册登录授权.但是这样会授予你QQ,微信的使用权限. 如你要证明房子是自己的,只要出示房产证即可.而若是你给钥匙给别人开门.则授予了别人进入房子的权限.
SpringSecurity的功能
作用
1.认证:用户登录,解决的是”你是谁”
2.授权:判断用户拥有什么权限,可以访问什么资源,解决的是”你能干什么”
3.安全防护,防止跨站请求,session攻击等.
应用场景
1.传统web开发的项目的登录功能
2.用户授权功能
3.单一登录,同一时间同一账号只允许在一个地方登录
4.cas单点登录,即多个系统只要登录一次,就可以在多个系统中访问
5.集成oauth2,做登录授权,可以用于app登录和第三方登录(QQ,微信等)
入门案例
创建一个简单的Controller
@RestController
public class HelloSecurityController {
@RequestMapping("/hello")
public String hello() {
return "Hello Security";
}
}
集成SpringSecurity,启动项目,访问接口,即可访问到SpringSecurity默认提供的页面.
认证基本原理
SpringSecurity功能的实现主要是由一系列过滤器相互配合完成,也称为过滤器链
过滤链
默认加载15个过滤器,可以自行增加删除自定义过滤器.
1.WebAsyncManagerIntegrationFilter
根据请求封装获取WebAsyncManager,从WebAsyncManager获取/注册的安全上下文可调用处理器
2.SecurityContextPersistenceFilter
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用,同时,存储了当前用户认证以及权限信息
3.HeaderWriterFilter
向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
4.CsrfFilter
csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的
token信息,如果不包含,则报错。起到防止csrf攻击的效果。
5.LogoutFilter
匹配URL为/logout的请求,实现用户退出,清除认证信息。
6.UsernamePasswordAuthenticationFilter
表单认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。
7.DefaultLoginPageGeneratingFilter
如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。
8.DefaultLogoutPageGeneratingFilter
由此过滤器可以生产一个默认的退出登录页面
9.Basicauthenticationfilter
此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。
10.RequestCacheAwareFilter
通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存
HttpServletRequest
11.SecurityContextHolderAwareRequestFilter
针对ServletRequest进行了一次包装,使得request具有更加丰富的API
12.AnonymousAuthenticationFilter
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到
SecurityContextHolder中。spring security为了兼容未登录的访问,也走了一套认证流程,
只不过是一个匿名的身份。
13.SessionManagementFilter
securityContextRepository限制同一用户开启多个会话的数量
14.ExceptionTranslationFilter
异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异
常
15.FilterSecurityInterceptor
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其
是否有权限。
认证方式
HttpBasic认证
HttpBasic模式要求传输的用户名密码使用Base64模式进行加密,如果用户名是 “admin” , 密码是“ admin”,则将字符串”admin:admin” 使用Base64编码算法加密。加密结果可能是: YWtaW46YWRtaW4=。HttpBasic模式真的是非常简单又简陋的验证模式,Base64的加密算法是 可逆的,想要破解并不难.
formLogin登录模式
Spring Security的HttpBasic模式,只是进行了通过携带Http的Header进行 简单的登录验证,而且没有定制的登录页面,Security 5.x版本默认会生成一个登录页面
表单认证
编写Security配置类,启动项目,访问页面即会进入登录页
/**
* Security配置类
* @author 12492 公众号:一只会飞的旺旺
* @date 2022-10-10 21:09
*/
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* http请求处理方法
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.httpBasic() //开启httpBasic认证
// .and().authorizeRequests()
// .anyRequest().authenticated(); //所有请求都需要登录认证才能访问
http.formLogin() //开启表单登录
.and().authorizeRequests()
.anyRequest().authenticated(); //所有请求都需要登录认证才能访问
}
}
常见问题
1.localhost将您重定向次数过多
配置登录页面放行
http.formLogin().loginPage("login.html") //开启表单登录,指定登录页面
.and().authorizeRequests()
.antMatchers("/login.html").permitAll() //登录页面不需要认证
.anyRequest().authenticated(); //所有请求都需要登录认证才能访问
2.访问login.html 报404错
通过springboot整合thymeleaf生成静态页面,所以需要请求接口生成模板页面,修改login.html为toLoginPage
protected void configure(HttpSecurity http) throws Exception {
// http.httpBasic() //开启httpBasic认证
// .and().authorizeRequests()
// .anyRequest().authenticated(); //所有请求都需要登录认证才能访问
http.formLogin().loginPage("toLoginPage") //开启表单登录
.and().authorizeRequests()
.antMatchers("/toLoginPage").permitAll() //登录页面不需要认证
.anyRequest().authenticated(); //所有请求都需要登录认证才能访问
}
3.访问login.html 后发现页面没有相关样式
放行静态资源,重写另外一个configure方法
/**
* web安全配置
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
// 设置不拦截规则
web.ignoring().antMatchers("/css/**", "/fonts/**", "/img/**", "/js/**","/favicon.ico");
}
注意: HttpSecurity和WebSecurity的区别
1.WebSecurity 不仅通过 HttpSecurity 定义某些请求的安全控制,也通过其他方式定义其他某些 请求可以忽略安全控制;
2.HttpSecurity 仅用于定义需要安全控制的请求(当然 HttpSecurity 也可以指定某些请求不需要 安全控制);
3.可以认为 HttpSecurity 是 WebSecurity 的一部分, WebSecurity 是包含 HttpSecurity 的更大 的一个概念;
4.构建目标不同
- WebSecurity 构建目标是整个 Spring Security 安全过滤器 FilterChainProxy
- HttpSecurity 的构建目标仅仅是 FilterChainProxy 中的一个 SecurityFilterChain 。
表单登录
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/toLoginPage") //开启表单登录
.loginProcessingUrl("/login") //登录表单提交地址
.usernameParameter("username") //登录表单用户名参数名
.passwordParameter("password") //登录表单密码参数名
.successForwardUrl("/") //登录成功跳转地址
.and().authorizeRequests()
.antMatchers("/toLoginPage").permitAll() //登录页面不需要认证
.anyRequest().authenticated(); //所有请求都需要登录认证才能访问
//关闭csrf防护
http.csrf().disable();
//允许iframe嵌套
http.headers().frameOptions().disable();
}
基于数据库实现认证
重写认证管理的configure方法
/**
* 认证信息管理
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new MyUserDetailsService());
}
自定义用户认证实现类
/**
* 基于数据库完成认证
*
* @author 12492 公众号:一只会飞的旺旺
* @date 2022-10-10 21:46
*/
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
/**
* 根据username查询用户实体
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if (null == user) {
throw new UsernameNotFoundException("用户不存在");
}
// 先声明一个权限集合
List<GrantedAuthority> authorities = new ArrayList<>();
// 简单构造
//return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
// 复杂构造
return new org.springframework.security.core.userdetails.User(user.getUsername(),
"{noop}" + user.getPassword(), // {noop}表示不加密
true, // 账号是否可用
true, // 账号是否过期
true, // 密码是否过期
true, // 账号是否锁定
authorities);
}
}
密码加密:SpringSecurity中的PasswordEncoder就是对密码进行编码的工具接口
选择一种加密算法即可,常用的如BCrypt.
修改UserDetails构建时,密码返回方式.修改{noop}为{bcrypt}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if (null == user) {
throw new UsernameNotFoundException("用户不存在");
}
// 先声明一个权限集合
List<GrantedAuthority> authorities = new ArrayList<>();
// 简单构造
//return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
// 复杂构造
return new org.springframework.security.core.userdetails.User(user.getUsername(),
"{bcrypt}" + user.getPassword(), // {noop}表示不加密, {bcrypt}表示加密
true, // 账号是否可用
true, // 账号是否过期
true, // 密码是否过期
true, // 账号是否锁定
authorities);
}
获取当前登录用户
- SecurityContextHolder
保留系统当前的安全上下文SecurityContext,其中就包括当前使用系统的用户的信息。
- SecurityContext
安全上下文,获取当前经过身份验证的主体或身份验证请求令牌
代码实现:
/**
* 获取当前登录用户信息
*
* @return
*/
@RequestMapping("/loginUser1")
@ResponseBody
public UserDetails getCurrentUser1() {
return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
/**
* 获取当前登录用户信息
*
* @return
*/
@RequestMapping("/loginUser2")
@ResponseBody
public UserDetails getCurrentUser2(Authentication authentication) {
return (UserDetails) authentication.getPrincipal();
}
/**
* 获取当前登录用户信息
*
* @return
*/
@RequestMapping("/loginUser3")
@ResponseBody
public UserDetails getCurrentUser3(@AuthenticationPrincipal UserDetails
userDetails) {
return userDetails;
}
remember me 记住我
1.简单的Token生成方法
Token=MD5(username+分隔符+expiryTime+分隔符+password)
后台开启remember-me功能
http.formLogin().loginPage("/toLoginPage") //开启表单登录
.loginProcessingUrl("/login") //登录表单提交地址
.usernameParameter("username") //登录表单用户名参数名
.passwordParameter("password") //登录表单密码参数名
.successForwardUrl("/") //登录成功跳转地址
.and().authorizeRequests()
.antMatchers("/toLoginPage").permitAll() //登录页面不需要认证
.anyRequest().authenticated() //所有请求都需要登录认证才能访问
.and()
.rememberMe()// 开启记住我功能
.tokenValiditySeconds(2000)// token失效时间默认两周
.rememberMeParameter("rememberMe");// 自定义表单name值
2.持久化Token生成方法
存入数据库Token包含:
token: 随机生成策略,每次访问都会重新生成
series: 登录序列号,随机生成策略。用户输入用户名和密码登录时,该值重新生成。使用 remember-me功能,该值保持不变
expiryTime: token过期时间。
CookieValue=encode(series+token)
后台开启remember-me功能
添加如下配置
添加持久化token方法
/**
* 持久化token,负责token与数据库之间的相关操作
*
* @return
*/
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new
JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);//设置数据源
// 启动时创建一张表, 第一次启动的时候创建, 第二次启动的时候需要注释掉, 否则会报错
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
注意
为了防止Cookie伪造处理.可以在重要接口添加如下验证,如
/**
* 根据用户ID查询用户
*
* @return
*/
@GetMapping("/{id}")
@ResponseBody
public User getById(@PathVariable Integer id) {
//获取认证信息
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
// 判断认证信息是否来自RememberMe
if (RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass())) {
throw new RememberMeAuthenticationException("认证信息来源于 RememberMe,请重新登录");
}
return userService.getById(id);
}
自定义登录成功和失败处理
自定义成功处理
实现AuthenticationSuccessHandler接口,并重写onAnthenticationSuccesss()方法.
自定义失败处理
实现AuthenticationFailureHandler接口,并重写onAuthenticationFailure()方法
SecurityConfiguration配置
.successHandler(myAuthenticationService)//自定义登录成功处理
.failureHandler(myAuthenticationService)//自定义登录失败处理
退出登录
LogoutFilter: 匹配URL为/logout的请求,实现用户退出,清除认证信息
图形验证码
Spring Security的认证校验是由UsernamePasswordAuthenticationFilter过滤器完成的,所以我们 的验证码校验逻辑应该在这个过滤器之前。验证码通过后才能到后续的操作.
自定义验证码过滤器
添加配置
创建验证码过滤器
/**
* 验证码验证filter 需要继承OncePerRequestFilter确保在一次请求只通过一次filter,而不
* 需要重复执行
*
* @author pengwangwang
* @date 2022/10/11 09:39
**/
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 判断是否是登录请求
if ("/login".equals(httpServletRequest.getRequestURI()) && "POST".equals(httpServletRequest.getMethod())) {
try {
validate(httpServletRequest);
} catch (ValidateCodeException e) {
e.printStackTrace();
return;
}
}
//如果不是登录请求,直接调用后面的过滤器链
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
public void validate(HttpServletRequest request) throws ValidateCodeException {
// 获取ip
String ip = request.getRemoteAddr();
//拼接redis的key
String key = "code_" + ip;
// 获取redis中的验证码
String code = stringRedisTemplate.opsForValue().get(key);
String redisImageCode = request.getParameter("imageCode");
if (StringUtils.hasText(redisImageCode)) {
throw new ValidateCodeException("验证码不能为空");
}
if (redisImageCode == null) {
throw new ValidateCodeException("验证码已过期");
}
if (!redisImageCode.equals(code)) {
throw new ValidateCodeException("验证码不正确");
}
// 从redis中移除imageCode
stringRedisTemplate.delete(key);
}
}
自定义验证码异常类
/**
* 验证码异常类
* @author pengwangwang
* @date 2022/10/11 09:42
**/
public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg) {
super(msg);
}
}
session管理
会话超时
在spring配置文件application.properties中添加超时配置
# session配置超时时间
server.servlet.session.timeout=60
修改自定义设置session超时后地址
并发控制
并发控制即同一个账号同时在线个数,同一个账号同时在线个数如果设置为1表示,该账号在同一时间内只能有一个有效的登录,如果同一个账号又在其它地方登录,那么就将上次登录的会话过期,即后面的登录会踢掉前面的登录.
1.配置session超时时间
# session配置超时时间
server.servlet.session.timeout=600
2.设置最大会话数量
3.阻止用户第二次登录
集群Session
实际场景中,一般一个服务至少会有两台服务器提供服务,前面由nginx进行负载均衡,但是这样会导致在一个服务器登录之后,session无法进行共享,导致访问另一台服务器时,需要重新登录
为了解决这一问题,我们通常会将用户登录的会话信息,保存到第三方库中,如redis,mongodb,mysql等.这样所有服务器都能从同一个库去获取当前的登录信息.就不需要再次登录了.
1.引入依赖
<!-- 基于redis实现session共享 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
2.设置session存储类型
#使用redis共享session
spring.session.store-type=redis
3.进行测试
csrf防护机制
CRSF是什么?
CSRF(Cross-site request forgery),中文名称:跨站请求伪造 你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的 事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账……造成的问 题包括:个人隐私泄露以及财产安全。
CRSF原理
1.登录受信任网站A,并在本地生成Cookie。
2.在不登出A的情况下,访问危险网站B。
3.触发网站B中的一些元素
防御策略
1.验证HTTP Referer字段
2.在请求地址中添加token并验证
3.在HTTP头中自定义属性并验证
开启csrf防御
跨域处理
配置跨域信息
/**
* 跨域配置信息源
*
* @return
*/
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 设置允许跨域的站点
corsConfiguration.addAllowedOrigin("*");
// 设置允许跨域的http方法
corsConfiguration.addAllowedMethod("*");
// 设置允许跨域的请求头
corsConfiguration.addAllowedHeader("*");
// 允许带凭证
corsConfiguration.setAllowCredentials(true);
// 对所有的url生效
UrlBasedCorsConfigurationSource source = new
UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
开启跨域配置