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)
}
});
}