一.SpringSecurity基础
1.基础实例代码
-
引入SpringSecurity配置
implementation 'org.springframework.boot:spring-boot-starter-security'
-
SpringSecurity基础配置
package com.example.demo.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { //WebSecurityConfigurerAdapter是web应用安全配置的适配器类 @Override protected void configure(HttpSecurity http) throws Exception { // http.formLogin()//使用表单登录(身份认证的方式)——会走UsernamePasswordAuthenticationFilter http.httpBasic()//基础弹窗方式方式——会走BasicAuthenticationFilter .and() //会走FileterSecurityInteceptor决定是否可以访问到REST API //用ExceptionTranslationFilter处理FilterSecurityInteceptor抛出的异常 .authorizeRequests()//设置认证授权 .anyRequest()//针对任何请求 .authenticated();//都需要进行认证授权 } }
2.SpringSecurity基本原理
-
核心概念:一组过滤器,所有访问的请求都会经过过滤器(SpringSecurity自动添加)
-
基本原理流程图
-
过滤器
- UsernamePasswordAuthenticationFilter用于接受\login的POST请求中的username和password
- ExceptionTranslationFilter用于接收异常并重定向到对应页面
- FilterSecurityIntecepter用于查看配置的授权机制是否满足,没满足则会走到ExceptionTranslationFilter
-
通常发送一个http请求时的处理流程:
- 用户请求指定url,因为没有发送对应\login等的请求所以会通过前面的filter,经过FilterSecurityInteceptor时查看拦截机制是所有的认证授权都拦截抛出异常,则会到ExceptionTranslationFilter进行重定向,之后会到登录页面,点登陆之后会经过UsernamePasswordAuthenticationFilter,填完用户名和密码后又回到FilterSecurityIntecepter,此时没有异常了则会到REST API中
3.自定义用户认证逻辑
-
使用原生的SpringSecurity会生成随机密码,而真实需求应该是使用数据库中的用户名和密码,则需要自定义
-
处理用户信息获取逻辑[UserDetailsService]
-
需要实现UserDetailsService接口,此接口中有loadUserByUsername方法接收username的参数,返回UserDetails对象(UserDetails也是一个接口),抛出的异常是UsernameNotFoundException[找不到对应用户异常]
-
简易实现
package com.example.demo.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; @Component public class MyUserDetailService implements UserDetailsService { //此处可以注入mapper并查询数据库 //添加logger Logger logger = LoggerFactory.getLogger(getClass()); @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //可以在此处根据用户名查找用户信息 //打印进入信息 logger.info("登录用户名:"+username); //User是SpringSecurity的类,已经实现UserDetails接口 return new User(username,"12345", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));//参数三表示用户的权限,会根据FilterSecurityIntecepter的安全配置授权的代码去验用户的权限是否满足 //AuthorityUtils.commaSeparatedStringToAuthorityList方法可以将权限字符串转换成权限对象 } }
-
-
处理用户校验逻辑[UserDetails接口]
-
UserDetails接口是用于定义用户信息的,封装了SpringSecurity登录的时候需要的信息
- getAuthorities()权限信息
- getPassword()密码
- getUsername()用户名
- 执行自己校验逻辑的方法:账户没有过期,没有用户过期概念可以永远返回true
boolean isAccountNonExpired()
- 执行自己校验逻辑的方法:账户没有被锁定
boolean isAccountNonLocked()
- 执行自己校验逻辑的方法:密码没有过期
boolean isCredentialNonExpired()
- 执行自己校验逻辑的方法:账户是否可用
boolean isEnabled()
-
实例演示
-
-
处理密码加密解密[PasswordEncoder接口]
-
PasswordEncoder接口中有两个方法
- encode():用来加密密码
- matches():用来判断加密后的代码跟用户传递的代码是否匹配
-
实例代码
-
在ConfigurerAdapter实现类中添加PasswordEncoder的实现类注入
package com.example.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { //WebSecurityConfigurerAdapter是web应用安全配置的适配器类 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin()//使用表单登录(身份认证的方式) //http.httpBasic()//基础弹窗方式方式 .and() .authorizeRequests()//设置认证授权 .anyRequest()//针对任何请求 .authenticated();//都需要进行认证授权 } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder();//passwordencoder的实现类 } }
-
在UserDetailsService实现类接口中添加返回用户判断的接口(此处使用再次加密的字符串模拟,实际上直接拿到数据库中加密的字符串即可)
package com.example.demo.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @Component public class MyUserDetailService implements UserDetailsService { //此处可以注入mapper并查询数据库 //添加logger Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //可以在此处根据用户名查找用户信息 //打印进入信息 logger.info("登录用户名:"+username); //User是SpringSecurity的类,已经实现UserDetails接口 //return new User(username,"12345", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));//参数三表示用户的权限,会根据FilterSecurityIntecepter的安全配置授权的代码去验用户的权限是否满足 //AuthorityUtils.commaSeparatedStringToAuthorityList方法可以将权限字符串转换成权限对象 logger.info("数据库密码是:"+passwordEncoder.encode("12345")); //查看用户是否被冻结或被锁定等操作则使用默认User的其他构造函数 return new User(username,passwordEncoder.encode("12345"),true,true,true,true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
-
-
附:使用自带的BCryptPasswordEncoder加密后的字符串每次都不一样,因为BCryptPasswordEncoder使用不同的key进行加密,防止攻击者通过密码和加密后的字符串反推其他密码,如调用了两次API,打印出来的密码是不相同的【当然可以自己实现PasswordEncoder接口,从而实现自定义的加密方式】
2019-02-23 12:45:55.270 INFO 1491 --- [nio-8088-exec-3] c.e.demo.config.MyUserDetailService : 登录用户名:111 2019-02-23 12:45:55.372 INFO 1491 --- [nio-8088-exec-3] c.e.demo.config.MyUserDetailService : 数据库密码是:$2a$10$iqXT5.EXEG8q9qctrlgjc.sqeLr61.B51qDB0JqXWOa/XN0wit9Ay 2019-02-23 12:46:22.215 INFO 1491 --- [nio-8088-exec-2] c.e.demo.config.MyUserDetailService : 登录用户名:111 2019-02-23 12:46:22.328 INFO 1491 --- [nio-8088-exec-2] c.e.demo.config.MyUserDetailService : 数据库密码是:$2a$10$miIZlVZPTsBrGt1M07RqneXAVtQNSeT/MQRGRCBYCIIpRA7HUMSpG
-
4.个性化用户认证流程
-
自定义登录页面
-
配置设置登录页面
package com.example.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { //WebSecurityConfigurerAdapter是web应用安全配置的适配器类 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin()//使用表单登录(身份认证的方式) .loginPage("/signin.html")//设置自定义登录页面 .loginProcessingUrl("/user/login")//设置登录页面的提交url给UsernamePasswordAuthenticationFilter的路径 //http.httpBasic()//基础弹窗方式方式 .and() .authorizeRequests()//设置认证授权 .antMatchers("/signin.html").permitAll()//当访问url时不需要身份认证,除了这个其他的请求都需要身份认证,此处如果不添加此通过则会无限重定向,因为每次访问这个地址时都需要验证验证的地址又会重定向回来 .anyRequest()//针对其他任何请求 .authenticated()//都需要进行认证授权 .and() .csrf().disable();//关闭csrf防御 } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder();//passwordencoder的实现类 } }
-
编写登录页面signin.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>登录</title> </head> <body> <p>自定义登录页面</p> <form action="/user/login"> 用户名: <input type="text" name="username"/> 密码: <input type="text" name="password"/> <input type="submit" value="登录" /> </form> </body> </html>
-
-
自定义登录成功/失败处理
-
将loginPage改成API形式,并可以获得访问的url并进行相关的重定向操作,并将跳转的page页面写到Properties文件中方便修改
-
实例代码
-
编写继承了WebSecurityConfigurerAdapter的Security配置文件[将loginpage跳转的路径设为API且将其添加到antMatchers中]
package com.example.demo.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SecurityProperties securityProperties; //WebSecurityConfigurerAdapter是web应用安全配置的适配器类 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin()//使用表单登录(身份认证的方式) .loginPage("/login")//设置自定义登录页面,此处更改为API,进而进行一次判断再跳转 .loginProcessingUrl("/user/login") //http.httpBasic()//基础弹窗方式方式 .and() .authorizeRequests()//设置认证授权 .antMatchers("/login",securityProperties.getLoginpage()).permitAll()//当访问url时不需要身份认证,除了这个其他的请求都需要身份认证 .anyRequest()//针对其他任何请求 .authenticated()//都需要进行认证授权 .and() .csrf().disable();//关闭csrf防御 } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder();//passwordencoder的实现类 } }
-
编写API实现打印访问请求url
package com.example.demo.controller; import com.example.demo.config.SecurityProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @RestController public class TestController { private RequestCache requestCache = new HttpSessionRequestCache();//用于获取上一个访问的请求 private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();//用于进行重定向 Logger logger = LoggerFactory.getLogger(getClass());//用于打日志 @Autowired private SecurityProperties securityProperties; @GetMapping("/test") public String test() { return "test string"; } @GetMapping("/login") @ResponseStatus(code = HttpStatus.UNAUTHORIZED)//返回状态码为401 public String LoginHandler(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException { SavedRequest savedRequest = requestCache.getRequest(httpRequest,httpResponse);//获取上一个请求对象 if(savedRequest != null){ String redirectUrl = savedRequest.getRedirectUrl();//获取上一个请求路径 logger.info("上一个请求路径:"+redirectUrl); redirectStrategy.sendRedirect(httpRequest,httpResponse,securityProperties.getLoginpage());//如果有上一个请求对象则重定向过去 } return "error"; } }
-
配置application.properties
server.port = 8088 security.loginpage = /signin.html
-
编写对应properties的java文件
package com.example.demo.config; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "security") public class SecurityProperties { private String loginpage; public String getLoginpage() { return loginpage; } public void setLoginpage(String loginpage) { this.loginpage = loginpage; } }
-
并在application.java文件中添加@EnableConfigurationProperties
package com.example.demo; import com.example.demo.config.SecurityProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @SpringBootApplication @EnableConfigurationProperties(SecurityProperties.class) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
-
二.深入SpringSecurity
1.认证流程源码详解
- 认证处理流程说明【参照SpringSceurity基本原理】
-
* 进来和出去时都经过SecurityContextPersistenceFilter查看是否有SecurityContext认证信息,有就放到session中【其中SecurityContextHolder相当于ThreadLocale,如果在同一个线程里都可以拿到对应内容】
-
认证结果如何在多个请求之间共享,查看源码可知都通过如下方式放到了SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(authentication);
-
获取认证用户信息,使用如下方式获取认证信息
- 方式一:使用SecurityContextHolder获取
@GetMapping("/me") public Authentication getAuthentication(){ return SecurityContextHolder.getContext().getAuthentication(); }
- 方式二:使用参数方式注入获得
@GetMapping("/me") public Authentication getAuthentication(Authentication authentication){ return authentication; }
- 方式三:只要UserDetails的信息,不要多余的信息,使用@AuthenticationPrincipal注解
@GetMapping("/me") public UserDetails getAuthentication(@AuthenticationPrincipal UserDetails user){ return user; }
2.添加自定义Filter
-
编写Filter类
public class ValidateCodeFilter extends OncePerRequestFilter{ //失败时处理类 private AuthenticationFilterHandler authenticationFilterHandler; //用于获取session private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @Override protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if(StringUtils.equals("/authentication/form",request.getRequestURI() && StringUtils.equalsIgnoreCase(request.getMethod(),"post"))){ try{ //自定义的验证方法,省略... validate(new ServletWebRequest(request)); }catch{ authenticationFilterHandler.onAuthenticationFailure(request,response,e); return; } } //执行接下来的过滤器 filterChain.doFilter(request,response); } ... }
-
添加addFilterBefore方法增加过滤器类
package com.example.demo.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SecurityProperties securityProperties; ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(); //WebSecurityConfigurerAdapter是web应用安全配置的适配器类 @Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class)//添加自定义filter .formLogin()//使用表单登录(身份认证的方式) .loginPage("/login")//设置自定义登录页面,此处更改为API,进而进行一次判断再跳转 .loginProcessingUrl("/user/login") //http.httpBasic()//基础弹窗方式方式 .and() .authorizeRequests()//设置认证授权 .antMatchers("/login",securityProperties.getLoginpage()).permitAll()//当访问url时不需要身份认证,除了这个其他的请求都需要身份认证 .anyRequest()//针对其他任何请求 .authenticated()//都需要进行认证授权 .and() .csrf().disable();//关闭csrf防御 } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder();//passwordencoder的实现类 } }