1 开发基于表单的认证
Spring security核心的功能
- 认证(你是谁?)
- 授权(你能干什么?)
- 攻击防护(防止伪造身份)
spring security实现了默认的用户名+密码认证,默认用户名为user,密码为:
spring security基本原理:过滤器链
对于UsernamePasswordAuthenticationFilter只会拦截 url为/login,method为POST的请求。
1.1 自定义用户认证逻辑
1)处理用户信息获取逻辑
UserDetailsService接口,只有一个方法:loadUserByUsername
实现该接口:数据库中存放的是加密密码,对于同一个密码不同时间的加密密文不一样
@Component public class MyUserDetailsService implements UserDetailsService { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { logger.info("用户名信息:" + s); // 根据用户名查找用户信息 logger.info("数据库密码:" + passwordEncoder.encode("123456")); // 用户名和密码信息用来做认证,权限信息用来对该用户做授权 return new User(s, "$2a$10$eFw06n0ABK2NFuse8y5f/eDUq7we26qQTceEtXSWNbMXnQ5Yf5Iha", AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
2)处理用户信息校验逻辑
处理密码加密解密:在配置文件中将PasswordEncoder对象注入spring容器,等价于@Component+包扫描组件
1.2 个性化用户认证流程
1)对于浏览器,返回自定义登录页面,让UsernamePasswordXxxFilter来处理登录请求;对于调用RESTful服务,返回json错误信息。
用户登录成功后 ,对于浏览器,返回需要的页面;对于服务,返回json数据。
权限配置:
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin()// 表单登录 .loginPage("/authentication/require") //将登录页面重定向到controller .loginProcessingUrl("/authentication/form") .and() .authorizeRequests() //请求授权 .antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage()).permitAll()//该页面允许通过 .anyRequest() .authenticated() // 其他资源需要认证 .and() .csrf().disable(); // 将跨站防护关掉 }
控制器,根据之前URL的路径判断是否为RESTful服务,在处理
/* 当客户端发出请求,当需要认证时,spring security会重定向到该控制器 */ @RestController public class BrowserSecurityController { private Logger logger = LoggerFactory.getLogger(getClass()); // 请求缓存 private RequestCache requestCache = new HttpSessionRequestCache(); private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Autowired private SecurityProperties securityProperties; /** * 当需要身份认证时跳转到这里 * @param request * @param response * @return */ @RequestMapping("/authentication/require") @ResponseStatus(code = HttpStatus.UNAUTHORIZED) public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException { // 判断请求类型,HTML或者app SavedRequest savedRequest = requestCache.getRequest(request, response); if(savedRequest!=null){ String targetUrl = savedRequest.getRedirectUrl(); logger.info("引发跳转的URL:"+targetUrl); // 如果之前的URL为.html结尾的URL,则重定向到登录页面 if(StringUtils.endsWithIgnoreCase(targetUrl, ".html")){ redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage()); } } return new SimpleResponse("请求的服务需要身份认证,请引导用户到登录页面"); } }
在启动项目中的application.properties文件中配置登录页面:
# 配置登录页面
getword.security.browser.loginPage=/demo.html
读取配置文件信息:
import org.springframework.boot.context.properties.ConfigurationProperties; // 读取前缀为getword.security的属性配置,其中browser中的属性会被读取到browserProperties中 @ConfigurationProperties(prefix = "getword.security") public class SecurityProperties { // browser的属性会匹配getword.security.browser后面的属性 private BrowserProperties browser = new BrowserProperties(); public BrowserProperties getBrowser() { return browser; } public void setBrowser(BrowserProperties browser) { this.browser = browser; } }
public class BrowserProperties { private String loginPage = "/login.html"; //默认值 public String getLoginPage() { return loginPage; } public void setLoginPage(String loginPage) { this.loginPage = loginPage; } }
@Configuration @EnableConfigurationProperties(SecurityProperties.class) //让属性配置读取器生效 public class SecurityCodeConfig { }
2)自定义登录成功处理,异步登录,AuthenticationSuccessHandler接口
自定义登录成处理:
@Component("vstudyAuthenticationSuccessHandler") public class VstudyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private Logger logger = LoggerFactory.getLogger(getClass()); //工具类, 将对象转成json @Autowired private ObjectMapper objectMapper; // 登录成功后调用 @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { logger.info("登录成功"); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(objectMapper.writeValueAsString(authentication)); } }
注册,使处理器生效:
3)登录失败处理
@Component("vstudyAuthenticationFailHandler") public class VstudyAuthenticationFailHandler implements AuthenticationFailureHandler { @Autowired private ObjectMapper objectMapper; private Logger logger = LoggerFactory.getLogger(getClass()); @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { logger.info("登录失败"); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); //服务器内部错误 response.setContentType("application/json;charset=utf-8"); response.getWriter().write(objectMapper.writeValueAsString(e)); } }
配置:和success类似
4)判断请求方式,做出相应的处理
successHandler:
@Component("vstudyAuthenticationSuccessHandler") public class VstudyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private Logger logger = LoggerFactory.getLogger(getClass()); //工具类, 将对象转成json @Autowired private ObjectMapper objectMapper; @Autowired private SecurityProperties securityProperties; //获取配置信息 // 登录成功后调用 @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { logger.info("登录成功"); if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){ response.setContentType("application/json;charset=utf-8"); response.getWriter().write(objectMapper.writeValueAsString(authentication)); }else{ // 调用父类方法,完成重定向跳转 super.onAuthenticationSuccess(request, response, authentication); } } }
failureHandler:
@Component("vstudyAuthenticationFailHandler") public class VstudyAuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler { @Autowired private SecurityProperties securityProperties; @Autowired private ObjectMapper objectMapper; private Logger logger = LoggerFactory.getLogger(getClass()); @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { logger.info("登录失败"); if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){ response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); //服务器内部错误 response.setContentType("application/json;charset=utf-8"); response.getWriter().write(objectMapper.writeValueAsString(e)); }else{ super.onAuthenticationFailure(request, response, e); } } }
2 认证流程
3 图形验证码
3.1 生成图形验证码
验证码图片信息:
public class ImageCode { private BufferedImage image; private String code; private LocalDateTime expireTime;//过期时间 public ImageCode(BufferedImage image, String code, int expireIn){ this.image = image; this.code = code; this.expireTime = LocalDateTime.now().plusSeconds(expireIn); } public ImageCode(BufferedImage image, String code, LocalDateTime expireTime){ this.image = image; this.code = code; this.expireTime = expireTime; } }
控制器:
@RestController public class ValidateCodeController { public static String SESSION_KEY = "SESSION_KEY_IMAGE_CODE"; private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @Autowired private SecurityProperties securityProperties; @GetMapping("/image/code") public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException { ImageCode imageCode = createImageCode(new ServletWebRequest(request,response)); sessionStrategy.setAttribute(new ServletWebRequest(request, response), SESSION_KEY, imageCode); ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream()); } /** * 生成ImageCode验证码 * @param request * @return */ public ImageCode createImageCode(ServletWebRequest request){ // 生成验证码,方法很多 int width = 60; int height = 20; BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics g = image.getGraphics(); Random random = new Random(); g.setColor(getRandColor(200, 250)); g.fillRect(0, 0, width, height); g.setFont(new Font("Times New Roman", 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); } String sRand = ""; for (int i = 0; i < 4; i++) { String rand = String.valueOf(random.nextInt(10)); sRand += 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, 100); } /** * 生成随机背景条纹 * * @param fc * @param bc * @return */ 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); } }
3.2 验证码校验
自定义过滤器:
public class ValidateCodeFilter extends OncePerRequestFilter { /** * 验证码校验失败处理器 */ private AuthenticationFailureHandler authenticationFailureHandler; /** * 系统配置信息 */ private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); /** * 系统中的校验码处理器 */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 只处理登录请求 if (StringUtils.equals("/authetication/form", request.getRequestURI()) && StringUtils.equalsIgnoreCase(request.getMethod(), "POST")) { try { logger.info("验证码校验通过"); } catch (ValidateCodeException e) { //验证失败 authenticationFailureHandler.onAuthenticationFailure(request, response, e); } } filterChain.doFilter(request, response); } protected void validate(ServletWebRequest request) throws ServletRequestBindingException { // 从session中拿到imageCode ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY); // 获取客户端输入的code,当前请求参数 String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode"); if(StringUtils.isBlank(codeInRequest)){ throw new ValidateCodeException("验证码不能为空"); } if(codeInSession==null){ throw new ValidateCodeException("验证码不存在"); } if(codeInSession.isExpired()){ throw new ValidateCodeException("验证码已过期"); } if(!StringUtils.equalsIgnoreCase(codeInSession.getCode(), codeInRequest)){ throw new ValidateCodeException("验证码不匹配"); } sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY); } public AuthenticationFailureHandler getAuthenticationFailureHandler() { return authenticationFailureHandler; } public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) { this.authenticationFailureHandler = authenticationFailureHandler; } }
配置:
执行流程:
/index.html ->redirect ->/authentication/require(控制器,判断是.html结尾)->login.html ->ValidateCodeFilter ->exception -> VstudyAuthenticationFailHandler ->loginType:JSON
login.html中,使用ajax发送登录请求 -> 验证码过滤器通过 -> UsernamePasswordFilter通过 -> 返回登录结果信息
3.3 验证码的接口
- 为了方便修改验证码的参数,如宽度、高度、长度等信息,我们将通过配置文件的形式配置这些信息。还有验证码的URL地址。
- 验证码拦截的接口可配置,比如为了限制用户操作频率,对用户操作使用验证码进行限制。验证码过滤器可以拦截多个控制器请求。
- 验证码的生成逻辑可以配置
三级配置:
1)验证码参数:
默认配置:
/** * 图形验证码 */ public class ImageCodeProperties { private int width = 67; private int height = 23; private int len = 4; private int expireIn = 60; //60秒后过期 //seter getter }
验证码信息:
/** * 验证码:包括图形验证码、短信验证码 */ public class ValidateCodeProperties { private ImageCodeProperties image; }
属性读取:
@ConfigurationProperties(prefix = "getword.security") public class SecurityProperties { // browser的属性会匹配getword.security.browser后面的属性 private BrowserProperties browser = new BrowserProperties(); // 验证码属性匹配getword.security.code后面的属性 private ValidateCodeProperties code = new ValidateCodeProperties(); }
end