Spring Security认证

Spring Security认证基本原理与两种认证方式

首先在工程中导入依赖

<!--添加Spring Security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

使用Spring Security框架会默认自动对我们系统中的资源进行保护,每次访问资源的时候,都必须进行一层身份校验,如果通过了就会重定向到我们输入的url中,否则,则会被拒绝访问。那么Spring Security是如何实现的呢,他的主要功能实现是通过一系列的过滤器互相配合完成,也被称作过滤器链。

过滤器链

在这里插入图片描述

认证方式

1、HttpBasic认证

HttpBasic认证可以说是SpringSecurity最简单的一种认证方式,他的目的不在于保证登录验证的绝对安全,而是一种防君子不防小人的认证方式,在早期的版本中Security 4.X版本,不需要任何配置,启动项目访问会弹出默认的HttpBasic认证,现在通常使用的SpingBoot2.x(依赖的Security5.x)的版本不再使用HttpBasic作为默认的认证方式,而是用表单认证作为默认的认证方式。

HttpBasic使用Base64模式对传输的账号密码进行加密,而Base64加密算法是可逆的,所以这是一种简陋的认证方式

2、fromLogin登录认证模式

SpringSecurity的HttpBasic认证模式比较简单,只是通过http携带的handler进行简单的登录验证,而且没有定制化的登录页面,而一个完整的应用系统,与验证相关的页面是高度定制化的,需要美观并且提供多种登录方式,这就需要用到SpringSecurity5.x支持的表单验证,它支持我们自定义登录页面,如果没有自定义页面也自动生成一个默认的登录页面。

表单认证

自定义表单登录

/**
* Security配置类
*/
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
http.formLogin()//开启表单认证
.loginPage("/toLoginPage")//自定义登录页面
.loginProcessingUrl("/login")// 登录处理Url
.usernameParameter("username").passwordParameter("password"). //
修改自定义表单name值.
.successForwardUrl("/")// 登录成功后跳转路径
.and().authorizeRequests().
antMatchers("/toLoginPage").permitAll()//放行登录页面与静态资源
.anyRequest().authenticated();//所有请求都需要登录认证才能访问;

// 允许iframe加载页面
http.headers().frameOptions().sameOrigin();
/**
* WebSecurity
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/css/**", "/images/**", "/js/**",
"/favicon.ico");
}
}

Spring Security中,安全构建器HTTPSecurity和WebSecurity的区别是
1、WebSecurity不仅通过HttpSecurity定义某些请求的安全配置,也可以通过其他方式定义其他某些请求可以忽略安全配置
2、HTTPSecurity仅用于定义需要安全控制的请求,当然也可以定义某些请求不需要安全控制
3、可以认为HTTPSecurity是WebSecurity的一部分,WebSecurity是包含HTTPSecurity的一个更大的概念
4、构建目标不同
WebSecurity构建目标是整个SpringSecurity安全过滤器FilterChainProxy
HTTPSecurity的构建目标仅是FilterChainProxy中的一个SecurityFilterChain

基于数据库实现认证功能

上面的表单认证所使用的用户名和密码是源于框架自动生成的,那么实际项目肯定不能是这样的,所以基于数据库要怎么搞呢?
首先我们要实现Security的UserDetailsService接口,重写里面的loadUserByUserName方法

/**
* 基于数据库中完成认证
*/
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
UserService userService;
/**
* 根据username查询用户实体
* *
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
User user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);// 用户名没有找到
}
// 先声明一个权限集合, 因为构造方法里面不能传入null
Collection<? extends GrantedAuthority> authorities = new ArrayList<>
();
// 需要返回一个SpringSecurity的UserDetails对象
UserDetails userDetails =
new
org.springframework.security.core.userdetails.User(user.getUsername(),
"{noop}" + user.getPassword(),// {noop}表示不加密认
证。
true, // 用户是否启用 true 代表启用
true,// 用户是否过期 true 代表未过期
true,// 用户凭据是否过期 true 代表未过期
true,// 用户是否锁定 true 代表未锁定
authorities);
return userDetails;
}
}

在SecurityConfiguration配置类中指定自定义认证

/**
* 身份验证管理器
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws
Exception {
auth.userDetailsService(myUserDetailsService);// 使用自定义用户认证
}

密码加密认证

在上面的验证中我们是明码验证的,也就是没有对密码进行加密验证,这样同样有一定的安全问题
在SpringSecurity中PasswordEncode是我们对密码进行编码的接口,该接口只有两个功能,一个是匹配验证,另一个是密码编码
使用密码加密就要看PassWordEncodeFactories密码器
在这里插入图片描述
这里就是不同的密码加密编码,noop代表的就是不加密,这里推荐使用BCrypt
我们使用的时候,只需要在这里进行替换即可
在这里插入图片描述
BCrypt算法介绍
任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。 有很多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。 Spring Security 提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。BCrypt强哈希方法 每次加密的结果都不一样,所以更加的安全。
bcrypt算法相对来说是运算比较慢的算法,在密码学界有句常话:越慢的算法越安全。黑客破解成本越高.通过salt和const这两个值来减缓加密过程,它的加密时间(百ms级)远远超过md5(大概1ms左右)。对于计算机来说,Bcrypt 的计算速度很慢,但是对于用户来说,这个过程不算慢。bcrypt是单向的,而且经过salt和cost的处理,使其受攻击破解的概率大大降低,同时破解的难度也提升不少,相对于MD5等加密方式更加安全,而且使用也比较简单

获取当前登录用户

在传统web系统中,我们将登录成功的用户放入到session中,在Security中我们又是如何去获取的session信息呢?
SpringSecurity提供了三种方法

/**
* 获取当前登录用户
*
* @return
*/
@RequestMapping("/loginUser1")
@ResponseBody
public UserDetails getCurrentUser() {
UserDetails userDetails = (UserDetails)
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userDetails;
}
/**
* 获取当前登录用户
*
* @return
*/
@RequestMapping("/loginUser2")
@ResponseBody
public UserDetails getCurrentUser(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails;
}
/**
* 获取当前登录用户
*
* @return
*/
@RequestMapping("/loginUser3")
@ResponseBody
public UserDetails getCurrentUser(@AuthenticationPrincipal UserDetails
userDetails) {
return userDetails;
}

Remmber me 记住我

这个功能想必小伙伴们都很熟悉,但是使用Security是如何去实现的呢?

简单的Token生成方式
在这里插入图片描述
Token=MD5(username+分隔符+expiryTime+password)
这种方法不推荐使用,因为密码信息是存放在前端浏览器cookie中的,所以会有安全问题
然后我们看一下实现,很简单
代码实现
1、前端代码 添加记住我复选框

<div class="form-group">
<div >
<!--记住我 name为remember-me value值可选true yes 1 on 都行-->
<input type="checkbox" name="remember-me" value="true"/>记住我
</div>
</div>

2、后台开启记住我功能

.and().rememberMe()//开启记住我功能
.tokenValiditySeconds(1209600)// token失效时间默认2周
.rememberMeParameter("remember-me")// 自定义表单name值

持久化Token实现方式
在这里插入图片描述
使用持久化token方式,经过配置以后Security会在项目第一次启动的时候自动生成一张表,用来存储token信息,但是记住在第二次启动之前将生成表的配置删除,否则会报错。
存入数据库的token信息包括:
token:随机生成,每次访问都会生成一条新的
series:登录序列号,随机生成,用户输入用户名和密码登录是重新生成,但是如果使用remmber me时,就不会重新生成
expirytime:失效时间
代码实现
前端不变
后端代码

/**
* http请求处理方法
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//开启表单认证
............
.and().rememberMe()//开启记住我功能
.tokenValiditySeconds(1209600)// token失效时间默认2周
.rememberMeParameter("remember-me")// 自定义表单name值
.tokenRepository(getPersistentTokenRepository());// 设置
tokenRepository
............
}
@Autowired
DataSource dataSource;
/**
* 持久化token,负责token与数据库之间的相关操作
*
* @return
*/
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new
JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);//设置数据源
// 启动时创建一张表, 第一次启动的时候创建, 第二次启动的时候需要注释掉, 否则
会报错
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}

项目启动成功后会为我们新建一个名为persistent_logins的表

但是无论是使用哪种方式都会在cookie中存储一份,那么就有cookie伪造的可能性
所以我们在一些重要的方法上还要再加上一些判断
安全验证

/**
* 根据用户ID查询用户
*
* @return
*/
@GetMapping("/{id}")
@ResponseBody
public User getById(@PathVariable Integer id) {
//获取认证信息
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
// 判断认证信息是否来源于RememberMe
if
(RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClas
s())) {
throw new RememberMeAuthenticationException("认证信息来源于
RememberMe,请重新登录");
}
User user = userService.getById(id);
return user;
}

这样我们正常的表单访问没有问题,但是如果是通过cookies直接访问就会跳转到登录页,要求重新登录

退出登录

配置路径为/logout请求,实现用户退出,清空认证信息

前端

<a class="button button-little bg-red" href="/logout">
<span class="icon-power-off"></span>退出登录</a></div>

后端

/**
* 自定义登录成功,失败,退出处理类
*/
@Service
public class MyAuthenticationService implements
AuthenticationSuccessHandler,
AuthenticationFailureHandler, LogoutSuccessHandler {
private RedirectStrategy redirectStrategy = new
DefaultRedirectStrategy();
................
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication) throws
IOException, ServletException {
System.out.println("退出成功后续处理....");
redirectStrategy.sendRedirect(request, response, "/toLoginPage");
}
}

SecurityConfig配置 protected void configure(HttpSecurity http) 方法

.and().logout().logoutUrl("/logout")//设置退出url
.logoutSuccessHandler(myAuthenticationService)//自定义退出处理

Session管理

SpringSecurity可以和Spring Session库配合使用,只需要做一些简单的配置就可以实现一些功能

会话超时

1、配置会话超时时间,默认为30分钟,但是springboot会话超时时间至少为60秒

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

当session会话过期后跳转到登录页面
2、设置session过期后的地址

http.sessionManagement()//设置session管理
.invalidSessionUrl("/toLoginPage")// session无效后跳转的路径,
默认是登录页面

并发控制

并发控制既同一个账号同时在线个数,同一个账号同时在线个数如果设置为1表示,这个账号在同一时间只能有一个有效登录。如果同一个账号又在其他地方登录,那么就会将上一个登录的会话过期,也就是会踢掉前面的登录会话

设置最大会话数量并阻止用户第二次登录

http.sessionManagement().//设置session管理
invalidSessionUrl("/toLoginPage")// session无效后跳转的路径, 默
认是登录页面
.maximumSessions(1)//设置session最大会话数量 ,1同一时间只能有一个
用户登录
.maxSessionsPreventsLogin(true)//当达到最大会话个数时阻止登录。
.expiredUrl("/toLoginPage");//设置session过期后跳转路径

集群session

这个和Security关系不大,主要是spring session但是这里也顺便说一下,很简单的配置
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

csrf防护机制

csrf:跨站请求伪造,就是说盗用你的身份,用你的名义发起恶意攻击

CSRF原理

在这里插入图片描述
从上图可以看出,要完成一次CSRF攻击,受害者要依次完成以下步骤
1、登录受信任的网址A,并生成本地cookies
2、在不登出A的前提下,访问危险网址B
3、触发B的一些元素

CSRF防御策略

SpringSecurity会对所有的post请求验证是否包含系统生成的csrf的token信息,如果不包含则报错,起到防止csrf攻击的效果
1、开启csrf防护(默认是开启的,不需要设置)
但是我们也可以关闭防护:http.csrf().disable();
还可以设置哪些方法是不需要保护

//可以设置哪些不需要防护
http.csrf().ignoringAntMatchers("/user/save");

2、页面需要添加token值

<input type="hidden" th:name="${_csrf.parameterName}"
th:value="${_csrf.token}"/>

跨域与CORS

跨域

跨域实际上是一种浏览器的保护机制,如果产生了跨域,服务器会在返回结果时,被浏览器拦截(注意:此时请求是可以正常发起的,只是浏览器对其结果进行了拦截),导致响应不可用,产生跨域的几种情况
在这里插入图片描述

解决跨域

JSONP
浏览器允许一些带src属性的标签跨域,也就是在某些标签上的src属性上写url地址不会产生跨域问题
CORS
cors是一个w3c标准,全称是跨域资源共享,cors需要浏览器和服务器共同支持,目前,所有浏览器都支持该功能,ie浏览器不低于IE10,浏览器在真正发起请求之前,会先发起一个OPTIONS类型的预检请求,用于请求服务器是否允许跨域,在得到许可后才发起请求

基于SpringSecurity的CORS支持

1、声明跨域配置源

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

2、开启跨域支持

//允许跨域
http.cors().configurationSource(corsConfigurationSource());

3、前端请求

function toCors() {
$.ajax({
// 默认情况下,标准的跨域请求是不会发送cookie的
xhrFields: {
withCredentials: true // 所以这里要配置一下带上cookie信息
},
url: "http://localhost:8090/user/1", // 登录url
success: function (data) {
alert("请求成功." + data)
}
});
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值