安全框架对比
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的加密算法是 可逆的,想要破解并不难.
Spring Security的HttpBasic模式,只是进行了通过携带Http的Header进行 简单的登录验证,而且没有定制的登录页面,Security 5.x版本默认会生成一个登录页面
表单认证
编写Security配置类,启动项目,访问页面即会进入登录页
@Configuration public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override protected void configure(HttpSecurity http) throws Exception {
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.formLogin().loginPage("toLoginPage") .and().authorizeRequests() .antMatchers("/toLoginPage").permitAll() .anyRequest().authenticated(); }
|
3.访问login.html 后发现页面没有相关样式
放行静态资源,重写另外一个configure方法
@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(); http.csrf().disable(); http.headers().frameOptions().disable(); }
|
基于数据库实现认证
重写认证管理的configure方法
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(new MyUserDetailsService()); }
|
自定义用户认证实现类
@Component public class MyUserDetailsService implements UserDetailsService {
@Autowired private UserService userService;
@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(), "{noop}" + user.getPassword(), 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); }
|
获取当前登录用户
保留系统当前的安全上下文SecurityContext,其中就包括当前使用系统的用户的信息。
安全上下文,获取当前经过身份验证的主体或身份验证请求令牌
代码实现:
@RequestMapping("/loginUser1") @ResponseBody public UserDetails getCurrentUser1() { return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); }
@RequestMapping("/loginUser2") @ResponseBody public UserDetails getCurrentUser2(Authentication authentication) { return (UserDetails) authentication.getPrincipal(); }
@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) .rememberMeParameter("rememberMe");
|
2.持久化Token生成方法
存入数据库Token包含:
token: 随机生成策略,每次访问都会重新生成 series: 登录序列号,随机生成策略。用户输入用户名和密码登录时,该值重新生成。使用 remember-me功能,该值保持不变 expiryTime: token过期时间。 CookieValue=encode(series+token)
|
后台开启remember-me功能
添加如下配置
添加持久化token方法
@Bean public PersistentTokenRepository getPersistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); tokenRepository.setCreateTableOnStartup(true); return tokenRepository; }
|
注意
为了防止Cookie伪造处理.可以在重要接口添加如下验证,如
@GetMapping("/{id}") @ResponseBody public User getById(@PathVariable Integer id) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 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过滤器完成的,所以我们 的验证码校验逻辑应该在这个过滤器之前。验证码通过后才能到后续的操作.
自定义验证码过滤器
添加配置
创建验证码过滤器
@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 { String ip = request.getRemoteAddr(); String key = "code_" + ip; 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("验证码不正确"); } stringRedisTemplate.delete(key); }
}
|
自定义验证码异常类
public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg) { super(msg); }
}
|
session管理
会话超时
在spring配置文件application.properties中添加超时配置
server.servlet.session.timeout=60
|
修改自定义设置session超时后地址
并发控制
并发控制即同一个账号同时在线个数,同一个账号同时在线个数如果设置为1表示,该账号在同一时间内只能有一个有效的登录,如果同一个账号又在其它地方登录,那么就将上次登录的会话过期,即后面的登录会踢掉前面的登录.
1.配置session超时时间
server.servlet.session.timeout=600
|
2.设置最大会话数量
3.阻止用户第二次登录
集群Session
实际场景中,一般一个服务至少会有两台服务器提供服务,前面由nginx进行负载均衡,但是这样会导致在一个服务器登录之后,session无法进行共享,导致访问另一台服务器时,需要重新登录
为了解决这一问题,我们通常会将用户登录的会话信息,保存到第三方库中,如redis,mongodb,mysql等.这样所有服务器都能从同一个库去获取当前的登录信息.就不需要再次登录了.
1.引入依赖
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
|
2.设置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; }
|
开启跨域配置