AI智能
改变未来

SpringSecurity基础场景应用大全


SpringSecurity应用

[toc]

pring Security 功能简介

  1. 认证:用户登录,两种认证方式:httpBasic、formLogin
  2. 授权:判断用户权限,可以访问什么资源
  3. 安全防护,防止跨站请求,session攻击等。

应用场景:

  1. 登录
  2. 授权
  3. 单一登录,一个账户同一时间只能在一个地方登录
  4. 集成cas,单点登录
  5. 集成oauth2,可以做第三方登录

基础入门

  1. 引入依赖:
<!--添加Spring Security 依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
  1. 再写一个简单的controller
@RestControllerpublic class HelloController {@RequestMapping("/hello")public String hello(){return "hello security";}}
  1. 访问http://localhost:8080/hello,发现自动跳到了登录页面。

  1. 进行登录,默认用户名:user,密码看启动日志
Using generated security password: bbb788d7-13aa-4e6d-9dc6-b7923a27e6a9

SpringSecurity认证基本原理

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

  1. org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter

根据请求封装获取WebAsyncManager,从WebAsyncManager获取/注册的安全上下文可调用处理拦截器2. org.springframework.security.web.context.SecurityContextPersistenceFilterSecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续fifilter建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。3. org.springframework.security.web.header.HeaderWriterFilter向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制4. org.springframework.security.web.csrf.CsrfFiltercsrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,如果不包含,则报错。起到防止csrf攻击的效果。5. org.springframework.security.web.authentication.logout.LogoutFilter

匹配URL为/logout的请求,实现用户退出,清除认证信息。6. org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter表单认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。7. org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。8. org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter由此过滤器可以生产一个默认的退出登录页面9. org.springframework.security.web.authentication.www.BasicAuthenticationFilter此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。10. org.springframework.security.web.savedrequest.RequestCacheAwareFilter通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest11. org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter针对ServletRequest进行了一次包装,使得request具有更加丰富的API12. org.springframework.security.web.authentication.AnonymousAuthenticationFilter当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。13. org.springframework.security.web.session.SessionManagementFiltersecurityContextRepository限制同一用户开启多个会话的数量14. org.springframework.security.web.access.ExceptionTranslationFilter异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常15. org.springframework.security.web.access.intercept.FilterSecurityInterceptor获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。

表单认证

3.1 自定义表单登录页

在config包下编写SecurityConfiguration配置类

@Configurationpublic class SecurityConfiguration extends WebSecurityConfigurerAdapter {@Overridepublic void configure(WebSecurity web) throws Exception {//对静态资源放行web.ignoring().antMatchers("/css/**", "/images/**", "/js/**","/favicon.ico");}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin()        //开启表单认证.loginPage("/toLoginPage").and().authorizeRequests().antMatchers("/toLoginPage").permitAll()   //登录请求不需要认证.anyRequest().authenticated();      //所有请求都需要认证}}

访问http://localhost:8080/,会自动跳转自定义的登录页面。

这时的登录页面的表单的请求参数必须得跟springSecurity默认的一致才行,那么可以自定义吗?答案是肯定的。

默认值:

自定义参数代码:

protected void configure(HttpSecurity http) throws Exception {http.formLogin()        //开启表单认证.loginPage("/toLoginPage").loginProcessingUrl("/login")  //登录处理url.usernameParameter("username")  //自定义用户名参数.passwordParameter("password")//自定义密码参数.defaultSuccessUrl("/")   //登录成功跳转路径.and().authorizeRequests().antMatchers("/toLoginPage").permitAll()   //登录请求不需要认证.anyRequest().authenticated();      //所有请求都需要认证http.csrf().disable();}
3.2 基于数据库实现认证功能

之前我们所使用的用户名和密码是来源于框架自动生成的, 那么我们如何实现基于数据库中的用户名和密码功能呢? 要实现这个得需要实现security的一个UserDetailsService接口, 重写这个接口里面loadUserByUsername即可

  1. 编写MyUserDetailsService并实现UserDetailsService接口,重写loadUserByUsername方法
@Servicepublic class MyUserDetailsService implements UserDetailsService {@Autowiredprivate UserService userService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userService.findByName(username);if(user==null){throw new UsernameNotFoundException(username);}// 先声明一个权限集合, 因为构造方法里面不能传入nullCollection<? extends GrantedAuthority> authorities = new ArrayList<>();UserDetails userDetails=new org.springframework.security.core.userdetails.User(username,"{noop}"+user.getPassword(),true,true,true,true,authorities);return userDetails;}}

在上面new UserDetails里面的代表的是加密方式为:不加密2. 在SecurityConfiguration配置类中指定自定义用户认证

@Configurationpublic class SecurityConfiguration extends WebSecurityConfigurerAdapter {@Autowiredprivate MyUserDetailsService myUserDetailsService;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(myUserDetailsService);}//省略部分代码...}
3.3 数据库用户密码加密认证

Spring Security 中 PasswordEncoder 就是我们对密码进行编码的工具接口。该接口只有两个功能:一个是匹配验证。另一个是密码编码。一般我们常用的算法是BCrypt算法。

BCrypt算法介绍

任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。 有很多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。 Spring Security 提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。BCrypt强哈希方法每次加密的结果都不一样,所以更加的安全。

bcrypt加密后的字符串形如:

$2a$10$wouq9P/HNgvYj2jKtUN8rOJJNRVCWvn1XoWy55N3sCkEHZPo3lyWq

其中$是分割符,无意义;2a是bcrypt加密版本号;10是const的值;而后的前22位是salt值;再然后的字符串就是密码的密文了;这里的const值即生成salt的迭代次数,默认值是10,推荐值12。

  1. 修改loadUserByUsername方法里的加密方式
@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userService.findByName(username);if(user==null){throw new UsernameNotFoundException(username);}// 先声明一个权限集合, 因为构造方法里面不能传入nullCollection<? extends GrantedAuthority> authorities = new ArrayList<>();UserDetails userDetails=new org.springframework.security.core.userdetails.User(username,"{bcrypt}"+user.getPassword(),true,true,true,true,authorities);return userDetails;}
  1. 修改数据库里的密码为加密格式

加密代码如下:

BCryptPasswordEncoder encoder=new BCryptPasswordEncoder();String encode = encoder.encode("123456");
3.4 获取当前登录用户

在传统web系统中, 我们将登录成功的用户放入session中, 在需要的时候可以从session中获取用户,那么Spring Security中我们如何获取当前已经登录的用户呢?

方式1:SecurityContextHolder

UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

方式2:Authentication

@RequestMapping("/getUser2")public UserDetails getUser2(Authentication authentication){UserDetails principal = (UserDetails) authentication.getPrincipal();return principal;}

方式3:@AuthenticationPrincipal

@RequestMapping("/getUser3")public UserDetails getUser3(@AuthenticationPrincipal UserDetails userDetails){return userDetails;}
3.5 remember me功能

在大多数网站中,都会实现RememberMe这个功能,方便用户在下一次登录时直接登录,避免再次输入用户名以及密码去登录,Spring Security针对这个功能已经帮助我们实现, 下面我们来看下他的原理图.

简单token方式

Token=MD5(username+分隔符+expiryTime+分隔符+password)

注意:这种方式不推荐使用,是将用户密码信息存在前端浏览器的cookie中,不安全。

实现方式:

  1. 前端页面需要增加remember-me的复选框
<div class="form-group">    <div >      <!--记住我 name为remember-me value值可选true yes 1 on 都行-->      <input type="checkbox"  name="remember-me" value="true"/>记住我    </div></div>
  1. 后端代码开启remember-me功能
.and().rememberMe()  //开启remeberMe功能.tokenValiditySeconds(60*60)        //token失效时间.rememberMeParameter("remember-me")     //自定义表单名称
  1. 验证,登录成功后查看cookie

登录成功后,关掉浏览器,再次访问,也不需要登录了。

持久化的Token生成方式

token: 随机生成策略,每次访问都会重新生成

series: 登录序列号,随机生成策略。用户输入用户名和密码登录时,该值重新生成。使用remember-me功能,该值保持不变

expiryTime: token过期时间。

CookieValue=encode(series+token)

  1. 后台代码
@Beanpublic PersistentTokenRepository persistentTokenRepository(){JdbcTokenRepositoryImpl tokenRepository=new JdbcTokenRepositoryImpl();tokenRepository.setDataSource(dataSource);//        启动时创建一张表, 第一次启动的时候创建, 第二次启动的时候需要注释掉, 否则//        会报错tokenRepository.setCreateTableOnStartup(true);return tokenRepository;}

注意: tokenRepository.setCreateTableOnStartup(true); 第一次设置为true,后面启动需要设置为false

  1. 配置持久化token
.and().rememberMe()  //开启remeberMe功能.tokenValiditySeconds(60*60)        //token失效时间.rememberMeParameter("remember-me")     //自定义表单名称.tokenRepository(persistentTokenRepository())  //设置tokenRepository
  1. 登录成功,查看数据

这两种方式都是依赖cookie的,如果cookie被窃取,会有安全问题。不需要登录,将cookie拷贝到postman里面就能够直接调用接口。

对重要的接口我们需要处理,处理方法如下:

public String hello(){//获取认证信息Authentication authentication = SecurityContextHolder.getContext().getAuthentication();//判断如果认证信息是来源于remember-me就拦截if(RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass())){throw new RememberMeAuthenticationException("请重写登录");}return "hello security";}
3.6 自定义登录成功或失败

有些时候登录成功或失败后需要做一些后续操作,比如日志收集,发送请求等。

自定义成功处理

实现AuthenticationSuccessHandler接口,并重写onAnthenticationSuccesss()方法

自定义失败处理

实现AuthenticationFailureHandler接口,并重写onAuthenticationFailure()方法

  1. 登录处理类
@Servicepublic class LoginHandler implements AuthenticationSuccessHandler,AuthenticationFailureHandler {private RedirectStrategy redirectStrategy = newDefaultRedirectStrategy();@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {System.out.println("登录失败");}@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {System.out.println("登录成功");redirectStrategy.sendRedirect(request,response,"/");}}
  1. 配置登录处理
.successHandler(loginHandler).failureHandler(loginHandler)

完整配置如下:

@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin()        //开启表单认证.loginPage("/toLoginPage").loginProcessingUrl("/login")  //登录处理url.usernameParameter("username")  //自定义用户名参数.passwordParameter("password")//自定义密码参数.defaultSuccessUrl("/")   //登录成功跳转路径.successForwardUrl("/").successHandler(loginHandler).failureHandler(loginHandler).and().rememberMe()  //开启remeberMe功能.tokenValiditySeconds(60*60)        //token失效时间.rememberMeParameter("remember-me")     //自定义表单名称.tokenRepository(persistentTokenRepository())  //设置tokenRepository.and().authorizeRequests().antMatchers("/toLoginPage").permitAll()   //登录请求不需要认证.anyRequest().authenticated();      //所有请求都需要认证http.csrf().disable();// 允许iframe加载页面http.headers().frameOptions().sameOrigin();}

异步登录

  1. 前端页面改造
<form id="formLogin" action="/login" method="post">        <div class="panel loginbox">         .....          <div style="padding:30px;">            <input type="button" onclick="login()"               class="button button-block bg-main text-big input-big" value="登录">          </div>        </div>      </form>    </div>  </div></div><script>  function login() {    $.ajax({      type: "POST",//方法类型      dataType: "json",//服务器预期返回类型      url: "/login",  // 登录url      data: $("#formLogin").serialize(),      success: function (data) {        console.log(data)        if (data.code == 200) {          window.location.href = "/";       } else {          alert(data.message);       }     }   }); }</script>
  1. 后端改造
@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {System.out.println("登录成功");Map result = new HashMap();result.put("code", HttpStatus.OK.value());// 设置响应码result.put("message", "登录成功");// 设置响应信息response.setContentType("application/json;charset=UTF-8");response.getWriter().write(objectMapper.writeValueAsString(result));}
3.7 退出登录

只需要发送请求,请求路径为/logout即可, 当然这个路径也可以自行在配置类中自行指定, 同时退出操作也有对应的自定义处理LogoutSuccessHandler,退出登录成功后执行,退出的同时如果有remember-me的数据,同时一并删除

  1. 前端页面
<a class="button button-little bg-red" href="/logout">      <span class="icon-power-off"></span>退出登录</a></div>
  1. 后端配置

实现LogoutSuccessHandler接口

@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {System.out.println("退出登录后续处理");}
.and().logout().logoutUrl("/logout")    //设置退出登录url.logoutSuccessHandler(loginHandler)     //自定义退出处理

图形验证码

  1. 验证码图片生成代码
@RequestMapping("/image/code")public void imageCode(HttpServletResponse response) throws IOException {ImageCode imageCode = createImageCode();BufferedImage image = imageCode.getImage();response.setContentType("image/jpeg");ImageIO.write(image,"jpeg",response.getOutputStream());}private ImageCode createImageCode() {int width = 100;    // 验证码图片宽度int height = 36;    // 验证码图片长度int length = 4;     // 验证码位数//创建一个带缓冲区图像对象BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);//获得在图像上绘图的Graphics对象Graphics g = image.getGraphics();Random random = new Random();//设置颜色、并随机绘制直线g.setColor(getRandColor(200, 250));g.fillRect(0, 0, width, height);g.setFont(new Font("宋体", Font.ITALIC, 20));g.setColor(getRandColor(160, 200));for (int i = 0; i < 155; i++) {int x = random.nextInt(width);int y = random.nextInt(height);int xl = random.nextInt(12);int yl = random.nextInt(12);g.drawLine(x, y, x + xl, y + yl);}//生成随机数 并绘制StringBuilder sRand = new StringBuilder();for (int i = 0; i < length; i++) {String rand = String.valueOf(random.nextInt(10));sRand.append(rand);g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));g.drawString(rand, 13 * i + 6, 16);}g.dispose();return new ImageCode(image, sRand.toString());}private Color getRandColor(int fc, int bc) {Random random = new Random();if (fc > 255) {fc = 255;}if (bc > 255) {bc = 255;}int r = fc + random.nextInt(bc - fc);int g = fc + random.nextInt(bc - fc);int b = fc + random.nextInt(bc - fc);return new Color(r, g, b);}
  1. 校验验证码是否正确

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

实现OncePerRequestFilter接口,写过滤其代码校验验证码。代码略

  1. 配置过滤器

将我们的过滤器配置在UsernamePassword校验前

 http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class);

Session管理

5.1 会话超时
  1. 配置session会话超时时间,默认30分钟
server.servlet.session.timeout=60s

注意:设置低于1分钟不起效

  1. 自定义设置session超时后跳转地址
http.sessionManagement() //设置session管理.invalidSessionUrl("/toLoginPage");
5.2 并发控制

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

http.sessionManagement() //设置session管理.invalidSessionUrl("/toLoginPage")             //session无效后跳转路径.maximumSessions(1)  //设置session最大会话数量,1代表同一时间只能一个用户登录.expiredUrl("/toLoginPage");        //session过期后跳转路径

阻止用户二次登录

sessionManagement也可以配置 maxSessionsPreventsLogin的值,当达到maximumSessions设置的最大会话个数时阻止登录。

http.sessionManagement() //设置session管理.invalidSessionUrl("/toLoginPage")             //session无效后跳转路径.maximumSessions(1)  //设置session最大会话数量,1代表同一时间只能一个用户登录.maxSessionsPreventsLogin(true)    //达到最大会话时阻止登录.expiredUrl("/toLoginPage");        //session过期后跳转路径
5.3 集群session

问题描述:实际场景中一个服务会至少有两台服务器在提供服务,在服务器前面会有一个nginx做负载均衡,用户访问nginx,nginx再决定去访问哪一台服务器。当一台服务宕机了之后,另一台服务器也可以继续提供服务,保证服务不中断。如果我们将session保存在Web容器(比如tomcat)中,如果一个用户第一次访问被分配到服务器1上面需要登录,当某些访问突然被分配到服务器二上,因为服务器二上没有用户在服务器一上登录的会话session信息,服务器二还会再次让用户登录,用户已经登录了还让登录就感觉不正常了。

解决这个问题有一个方案是将session共享,可以存在单独的地方(redis、数据库、mongodb等)。

  1. 引入依赖
<!-- 基于redis实现session共享 --><dependency>  <groupId>org.springframework.session</groupId>  <artifactId>spring-session-data-redis</artifactId></dependency>
  1. 设置session存储类型
#使用redis共享sessionspring.session.store-type=redis

csrf防护机制

什么是csrf?

CSRF(Cross-site request forgery),中文名称:跨站请求伪造

从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成三个步骤:

  1. 登录一个受信任网站A,并生成本地Cookie
  2. 在不登出A的情况下,访问危险网站B
  3. 网站B是个黑客做的网站,他里面有些链接和按钮会让用户调用A网站的接口去做一些对你不利的操作。

csrf的防御策略

  1. 验证http referer

接收到请求的时候判断是不是由自己网站发来的2. 在请求地址中添加tokon并验证

黑客之所以能完全伪造用户的请求,是因为验证用户的信息放在cookie里的,所以他可以利用你的cookie来通过安全校验。要抵御csrf,只要在请求里加入黑客不能伪造的信息就可以了。

比如在请求参数中加入一个token,并且在服务端建一个拦截器来校验这个token。这个token是服务器给前端的,黑客在别的网站上拿不到,这样就可以避免csrf攻击了。3. 在http头中自定义属性并验证

这种方法也是使用token来验证,和上一种方法不同的是是将token放在http头中。

security防御csrf

Security依靠org.springframework.security.web.csrf.CsrfFilter拦截器来进行验证token。

  1. 页面发请求时增加token值
<div><input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/></div>
  1. 后端(默认是开启防护的,如某接口不需要防护可以配置)
//哪些接口不做csrf防护http.csrf().ignoringAntMatchers("/user/save");

跨域

跨域,实际上是浏览器的一种保护处理,如果产生了跨域,服务器在返回结果时就会被浏览器拦截。

两个网站端口不同,域名不同,协议不同都会被认为是跨域。

解决跨域

  1. JSONP

浏览器允许一些带src属性的标签跨域,也就是在某些标签的src属性上写url地址是不会产生跨域问题2. CORS解决跨域

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。浏览器在发起真正的请求之前,会发起一个OPTIONS类型的预检请求,用于请求服务器是否允许跨域,在得到许可的情况下才会发起请求。

Security处理跨域

  1. 配置cors
public CorsConfigurationSource corsConfigurationSource() {CorsConfiguration corsConfiguration = new CorsConfiguration();// 设置允许跨域的站点corsConfiguration.addAllowedOrigin("*");// 设置允许跨域的http方法corsConfiguration.addAllowedMethod("*");// 设置允许跨域的请求头corsConfiguration.addAllowedHeader("*");// 允许带凭证corsConfiguration.setAllowCredentials(true);// 对所有的url生效UrlBasedCorsConfigurationSource source = newUrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", corsConfiguration);return source;}
http.cors().configurationSource(corsConfigurationSource());

再发请求,成功了

赞(0) 打赏
未经允许不得转载:爱站程序员基地 » SpringSecurity基础场景应用大全