安全框架对比

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默认提供的页面.

image-20221010204832030

认证基本原理

SpringSecurity功能的实现主要是由一系列过滤器相互配合完成,也称为过滤器链

过滤链

image-20221010205904052

默认加载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将您重定向次数过多

image-20221010212825408

配置登录页面放行

http.formLogin().loginPage("login.html") //开启表单登录,指定登录页面
.and().authorizeRequests()
.antMatchers("/login.html").permitAll() //登录页面不需要认证
.anyRequest().authenticated(); //所有请求都需要登录认证才能访问

2.访问login.html 报404错

image-20221010212959493

通过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 后发现页面没有相关样式

image-20221010213216908

放行静态资源,重写另外一个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生成方法

image-20221010221430517

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生成方法

image-20221010221715431

存入数据库Token包含:

token: 随机生成策略,每次访问都会重新生成 
series: 登录序列号,随机生成策略。用户输入用户名和密码登录时,该值重新生成。使用 remember-me功能,该值保持不变
expiryTime: token过期时间。
CookieValue=encode(series+token)

后台开启remember-me功能

添加如下配置

image-20221010221958796

添加持久化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()方法

image-20221011092638023

SecurityConfiguration配置

.successHandler(myAuthenticationService)//自定义登录成功处理
.failureHandler(myAuthenticationService)//自定义登录失败处理

退出登录

LogoutFilter: 匹配URL为/logout的请求,实现用户退出,清除认证信息

image-20221011093024202

图形验证码

Spring Security的认证校验是由UsernamePasswordAuthenticationFilter过滤器完成的,所以我们 的验证码校验逻辑应该在这个过滤器之前。验证码通过后才能到后续的操作.

image-20221011093844274

自定义验证码过滤器

添加配置

image-20221011095034118

创建验证码过滤器

/**
* 验证码验证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超时后地址

image-20221011101527246

并发控制

并发控制即同一个账号同时在线个数,同一个账号同时在线个数如果设置为1表示,该账号在同一时间内只能有一个有效的登录,如果同一个账号又在其它地方登录,那么就将上次登录的会话过期,即后面的登录会踢掉前面的登录.

1.配置session超时时间

# session配置超时时间
server.servlet.session.timeout=600

2.设置最大会话数量

3.阻止用户第二次登录

image-20221011101905284

集群Session

实际场景中,一般一个服务至少会有两台服务器提供服务,前面由nginx进行负载均衡,但是这样会导致在一个服务器登录之后,session无法进行共享,导致访问另一台服务器时,需要重新登录

image-20221011102300381

为了解决这一问题,我们通常会将用户登录的会话信息,保存到第三方库中,如redis,mongodb,mysql等.这样所有服务器都能从同一个库去获取当前的登录信息.就不需要再次登录了.

image-20221011102411525

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原理

image-20221012133623800

1.登录受信任网站A,并在本地生成Cookie。

2.在不登出A的情况下,访问危险网站B。

3.触发网站B中的一些元素

防御策略

1.验证HTTP Referer字段
2.在请求地址中添加token并验证
3.在HTTP头中自定义属性并验证

开启csrf防御

image-20221012134251219

跨域处理

配置跨域信息

/**
* 跨域配置信息源
*
* @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;
}

开启跨域配置

image-20221012134617938