• 2019-2-25


    一、《Spring Security开发安全的REST服务》视频笔记---part2:Spring Security部分

    Spring Security核心功能:认证(你是谁)、授权(你能干什么)、攻击防护(防止伪造身份)

    内容:Spring Security基本原理、实现用户名+密码认证、实现手机号+短信认证

     1、Spring Security开发基于表单的认证

    WebSecurityConfigurerAdapter是web安全应用的一个适配器,弄成表单登录配置以下即可(默认账号是user,密码随机生成):

    @Configuration
    public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
    	
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		
    		http.formLogin()
    			.and()
    			.authorizeRequests()
    			.anyRequest()
    			.authenticated();
    		
    	}
    	
    }
    

      若是还是想用回最原始的弹出框输入账号密码登录,则这样写:

    http.httpBasic()
    			.and()
    			.authorizeRequests()
    			.anyRequest()
    			.authenticated();
    

      

    Spring Security基本原理(对应视频4-2):

    上面的第一个filter对应于配置中的http.formLogin(),第二个filter对应于配置中的http.httpBasic()(注意:如果不配置,则这些filter不会生效;除了绿色以外,其它颜色的过滤器无法控制,即一定会在过滤器链上),最后一个Interceptor是请求要经过的最后一个过滤器(用来做最终判断的),对应于配置中每个and()后面的内容,若不符合这些内容的要求,则靠响应过程中的第二个filter(即从右边开始数的第二个)来捕获异常,并根据是否有异常来决定是否引导用户回到前面的配置中登录(比如前面配了用户名密码登录,则引导回那里登录)。

    2、自定义用户认证逻辑

    有3个步骤:处理用户信息获取逻辑(比如从数据库获取)[UserDetailsService]、处理用户校验逻辑(比如用户有无被冻结)[UserDetails]、处理密码加密解密[PasswordEncoder]

    (1)用户信息获取逻辑被SpringSecurity封装在一个接口中:UserDetailsService

    public interface UserDetailsService {
        UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
    }
    

      

    该接口只有一个loadUserByUsername方法,是根据用户名获取用户信息并将之封装到UserDetails接口的实现类中,SpringSecurity就拿着这个用户信息去做处理和校验,通过的话就放在session中;不通过就返回异常并有相应的错误提示。

    下面用自己定义的类去实现UserDetailsService接口:

    @Component
    public class MyUserDetailsService implements UserDetailsService{
        
            logger...
    
            @override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
    
                    logger.info("登录用户名:"+username);
                    return new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
            }      
    
    }    
    

      上面的User类是SpringSecurity提供的实现了UserDetails接口的类,该类的构造函数中第3个参数是权限集合,这里调用的方法是将逗号分隔的字符串转换成权限的方法(这里的权限会与上面配置类中需要的权限校验做对比,看看校验能否通过)。

    (2)处理用户校验逻辑(主要有这些校验:密码是否正确、用户是否被冻结、密码是否过期)

    校验用户的逻辑其实就放在UserDetails接口里的4个返回布尔值的方法:

    public interface UserDetails extends Serializable {
        Collection<? extends GrantedAuthority> getAuthorities();
    
        String getPassword();
    
        String getUsername();
    
        boolean isAccountNonExpired();
    
        boolean isAccountNonLocked();
    
        boolean isCredentialsNonExpired();
    
        boolean isEnabled();
    }
    

      isAccountNonExpired返回true代表账号没有过期,isAccountNonLocked返回true代表账号没有被锁定(通常与业务中的用户冻结划等号), isCredentialsNonExpiredfanhtrue代表密码没有过期,isEnabled代表账号是否可用(通常与业务中的用户逻辑删除即假删除画等号)。

     这里可以修改上面的User调用的构造函数:

    return new User(username, "123456", true, true , true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    

      或者可以自己定义一个与实际用户实体业务结合的User类去实现UserDetails接口。

    (3)处理密码加密解密

    找到PasswordEncoder类(是crypto包下的类),

    public interface PasswordEncoder {
        String encode(CharSequence var1);
    
        boolean matches(CharSequence var1, String var2);
    }
    

      

    encode方法用来将密码加密,matches方法用来判断用户提交上来的密码是否和用户的密码匹配。

    配置一个实现了PasswordEncoder接口的BCryptPasswordEncoder类,也可以自定义实现。

    @Configuration
    public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
    	
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		
    		http.formLogin()
    			.and()
    			.authorizeRequests()
    			.anyRequest()
    			.authenticated();
    		
    	}
    
            @Bean
    	public PasswordEncoder passwordEncoder() {
    		return new BCryptPasswordEncoder();
    	}
    	
    }
    

      这时候刚刚那个User就需要提供加密后的密码参数:

    return new User(username, passwordEncoder.encode("123456"), true, true , true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    

      

     3、个性化用户认证流程

    有3个步骤:自定义登录页面、自定义登录成功处理、自定义登录失败处理

    (1)自定义登录页面(在刚刚上面那个配置类补充一个loginPage方法调用),并在项目resources目录下再新建一个resources文件夹,里面放一个imooc-signIn.html

            @Override
    	protected void configure(HttpSecurity http) throws Exception {
    		
    		http.formLogin()
                            .loginPage("/imooc-signIn.html")
    			.and()
    			.authorizeRequests()
    			.anyRequest()
    			.authenticated();
    		
    	}
    

      但如果只是这样配置,进入这个登录页面会报错:重定向次数过多。这是因为现在的配置是:所有页面,包括这个登录页面,都需要身份认证,所以都要跳转到登录页面,即登录页面本身也需要身份认证也就需要再次跳转到自身。

    所以代码改成如下:

    @Override
    	protected void configure(HttpSecurity http) throws Exception {
    		
    		http.formLogin()
                            .loginPage("/imooc-signIn.html")
    			.and()
    			.authorizeRequests()
                            .antMatchers("/imooc-signIn.html").permitAll()
    			.anyRequest()
    			.authenticated();
    		
    	}
    

      imooc-signIn.html页面如下:

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>登录</title>
    </head>
    <body>
    	<h2>标准登录页面</h2>
    	<h3>表单登录</h3>
    	<form action="/authentication/form" method="post">
    		<table>
    			<tr>
    				<td>用户名:</td> 
    				<td><input type="text" name="username"></td>
    			</tr>
    			<tr>
    				<td>密码:</td>
    				<td><input type="password" name="password"></td>
    			</tr>
    			<tr>
    				<td>图形验证码:</td>
    				<td>
    					<input type="text" name="imageCode">
    					<img src="/code/image?width=200">
    				</td>
    			</tr>
    			<tr>
    				<td colspan='2'><input name="remember-me" type="checkbox" value="true" />记住我</td>
    			</tr>
    			<tr>
    				<td colspan="2"><button type="submit">登录</button></td>
    			</tr>
    		</table>
    	</form>
    	
    	<h3>短信登录</h3>
    	<form action="/authentication/mobile" method="post">
    		<table>
    			<tr>
    				<td>手机号:</td>
    				<td><input type="text" name="mobile" value="13012345678"></td>
    			</tr>
    			<tr>
    				<td>短信验证码:</td>
    				<td>
    					<input type="text" name="smsCode">
    					<a href="/code/sms?mobile=13012345678">发送验证码</a>
    				</td>
    			</tr>
    			<tr>
    				<td colspan="2"><button type="submit">登录</button></td>
    			</tr>
    		</table>
    	</form>
    	<br>
    	<h3>社交登录</h3>
    	<a href="/qqLogin/callback.do">QQ登录</a>
    	    
    	<a href="/qqLogin/weixin">微信登录</a>
    </body>
    </html>
    

      注意:该登录页面的登录请求是提交到/authentication/form,而不是SpringSecurity提供的UsernamePasswordAuthenticationFilter类指定的/login路径,那么现在需要UsernamePasswordAuthenticationFilter类去处理这个新的路径,所以需要配置:

    @Override
    	protected void configure(HttpSecurity http) throws Exception {
    		
    		http.formLogin()
                            .loginPage("/imooc-signIn.html")
                            .loginProcessingUrl("/authentication/form")
    			.and()
    			.authorizeRequests()
                            .antMatchers("/imooc-signIn.html").permitAll()
    			.anyRequest()
    			.authenticated();
    		
    	}
    

      按照这个配置,登录后会报另一个错误:Invalid CSRF Token 'null' was found on request parameter '_csrf' or header 'X-CSRF-TOKEN'

    因为SpringSecurity在默认情况下会提供一个跨站请求伪造的一个防护,可以暂时禁止这个配置:

    @Override
    	protected void configure(HttpSecurity http) throws Exception {
    		
    		http.formLogin()
                            .loginPage("/imooc-signIn.html")
                            .loginProcessingUrl("/authentication/form")
    			.and()
    			.authorizeRequests()
                            .antMatchers("/imooc-signIn.html").permitAll()
    			.anyRequest()
    			.authenticated()
                            .and()
                            .csrf().disable();
    		
    	}
    

      这样配置之后,登录逻辑可以走通,但是有2个不合理的地方:

    ①登录失败返回的是一个html页面而不是一个json字符串

    ②现在的配置是统一使用同一个登录页面,应该改成有自定义的登录页面就使用自定义的,没有的话才使用这个登录页面。

    先看第一个地方怎么改:

    目前的逻辑是当需要身份认证时跳到一个登录页面,应改成跳到一个自定义的Controller方法上:

    Controller自定义方法如下:

    @RestController
    public class BrowserSecurityController {
    
    	private Logger logger = LoggerFactory.getLogger(getClass());
    
    	private RequestCache requestCache = new HttpSessionRequestCache();
    
    	private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    
    	@Autowired
    	private SecurityProperties securityProperties;
    
    	@Autowired
    	private ProviderSignInUtils providerSignInUtils;
    
    	/**
    	 * 当需要身份认证时,跳转到这里
    	 */
    	@RequestMapping(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)
    	@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    	public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response)
    			throws IOException {
    
    		SavedRequest savedRequest = requestCache.getRequest(request, response);
    
    		if (savedRequest != null) {
    			String targetUrl = savedRequest.getRedirectUrl();
    			logger.info("引发跳转的请求是:" + targetUrl);
    			if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
    				redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
    			}
    		}
    
    		return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页");
    	}
    
    }
    

      

    配置这些类:

    让这些类生效:

    @Configuration
    @EnableConfigurationProperties(SecurityProperties.class)
    public class SecurityCoreConfig {
    
    }
    

      配置如下:

      

    @Override
    	protected void configure(HttpSecurity http) throws Exception {
    		
    		http.formLogin()
                            .loginPage("/imooc-signIn.html")
                            .loginProcessingUrl("/authentication/form")
    			.and()
    			.authorizeRequests()
                            .antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage()).permitAll()
    			.anyRequest()
    			.authenticated()
                            .and()
                            .csrf().disable();
    		
    	}
    

      

    (2)自定义登录成功处理

    关键接口:AuthenticationSuccessHandler

    自定义自己的ImoocAuthenticationSuccessHandler类实现AuthenticationSuccessHandler接口(或者也可以继承SavedRequestAwareAuthenticationSuccessHandler类---这是SpringSecurity默认的登录成功处理器)

    然后在配置类BrowserSecurityConfig中注入上面的自定义类:

    @Override
    	protected void configure(HttpSecurity http) throws Exception {
    		
    		http.formLogin()
                            .loginPage("/imooc-signIn.html")
                            .loginProcessingUrl("/authentication/form")
                            .successHandler(imoocAuthenticationSuccessHandler)
    			.and()
    			.authorizeRequests()
                            .antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage()).permitAll()
    			.anyRequest()
    			.authenticated()
                            .and()
                            .csrf().disable();
    		
    	}
    

      

    (3)自定义登录失败处理

    关键接口:AuthenticationFailureHandler

    自定义自己的ImoocAuthenticationFailureHandler类实现AuthenticationFailureHandler接口(或者也可以继承SimpleUrlAuthenticationFailureHandler类)

    然后在配置类BrowserSecurityConfig中注入上面的自定义类:

    @Override
    	protected void configure(HttpSecurity http) throws Exception {
    		
    		http.formLogin()
                            .loginPage("/imooc-signIn.html")
                            .loginProcessingUrl("/authentication/form")
                            .successHandler(imoocAuthenticationSuccessHandler)
                            .failureHandler(imoocAuthenticationFailureHandler)
    			.and()
    			.authorizeRequests()
                            .antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage()).permitAll()
    			.anyRequest()
    			.authenticated()
                            .and()
                            .csrf().disable();
    		
    	}
    

      

    4、认证流程源码级详解(把前面的内容可以联系起来理解)[对应视频4-6]

    有3块内容:认证处理流程说明、认证结果如何在多个请求之间共享、获取认证用户信息

    (1)认证处理流程说明

    认证流程中核心的类:

    (2)认证结果如何在多个请求之间共享

    该SecurityContextPersistenceFilter是过滤器链第一个过滤器,请求时检查session,如果有SecurityContext就放到线程中;响应时检查线程,如果有SecurityContext就拿出来放到session中。这样不同的请求,就可以从同一个session拿到相同的认证信息。

    (3)获取认证用户信息

            @GetMapping("/me")
    	public Object getCurrentUser() {
    		return SecurityContextHolder.getContext().getAuthentication();
    	}

    或者更简便的写法:

            @GetMapping("/me")
    	public Object getCurrentUser(Authentication authentication) {
    		return authentication;
    	}
    

      但是上面的写法是返回全部的authentication信息

    如果只想知道authentication信息里的principal信息部分,这样写:

            @GetMapping("/me")
    	public Object getCurrentUser(@AuthenticationPrincipal UserDetails user) {
    		return user;
    	}
    

      

    5、实现图形验证码功能

    有2步:开发生成图形验证码接口、在认证流程中加入图形验证码校验、重构代码

    (1)生成图形验证码:根据随机数生成图片、将随机数存到session中、将生成的图片写到接口的响应中

    验证码:

    public class ValidateCode {
    	
    	private String code;
    	
    	private LocalDateTime expireTime;
    	
    	public ValidateCode(String code, int expireIn){
    		this.code = code;
    		this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    	}
    	
    	public ValidateCode(String code, LocalDateTime expireTime){
    		this.code = code;
    		this.expireTime = expireTime;
    	}
    	
    	public boolean isExpried() {
    		return LocalDateTime.now().isAfter(expireTime);
    	}
    
    	public String getCode() {
    		return code;
    	}
    
    	public void setCode(String code) {
    		this.code = code;
    	}
    
    	public LocalDateTime getExpireTime() {
    		return expireTime;
    	}
    
    	public void setExpireTime(LocalDateTime expireTime) {
    		this.expireTime = expireTime;
    	}
    	
    }
    

      图片验证码:

    public class ImageCode extends ValidateCode {
    	
    	private BufferedImage image; 
    	
    	public ImageCode(BufferedImage image, String code, int expireIn){
    		super(code, expireIn);
    		this.image = image;
    	}
    	
    	public ImageCode(BufferedImage image, String code, LocalDateTime expireTime){
    		super(code, expireTime);
    		this.image = image;
    	}
    	
    	public BufferedImage getImage() {
    		return image;
    	}
    
    	public void setImage(BufferedImage image) {
    		this.image = image;
    	}
    
    }
    

      控制器方法:

    @RestController
    public class ValidateCodeController {
    
    	@Autowired
    	private ValidateCodeProcessorHolder validateCodeProcessorHolder;
    
    	/**
    	 * 创建验证码,根据验证码类型不同,调用不同的 {@link ValidateCodeProcessor}接口实现
    	 * 
    	 * @param request
    	 * @param response
    	 * @param type
    	 * @throws Exception
    	 */
    	@GetMapping(SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/{type}")
    	public void createCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type)
    			throws Exception {
    		validateCodeProcessorHolder.findValidateCodeProcessor(type).create(new ServletWebRequest(request, response));
    	}
    
    }
    

      检查验证码的filter:

    @Component("validateCodeFilter")
    public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
    
    	/**
    	 * 验证码校验失败处理器
    	 */
    	@Autowired
    	private AuthenticationFailureHandler authenticationFailureHandler;
    	/**
    	 * 系统配置信息
    	 */
    	@Autowired
    	private SecurityProperties securityProperties;
    	/**
    	 * 系统中的校验码处理器
    	 */
    	@Autowired
    	private ValidateCodeProcessorHolder validateCodeProcessorHolder;
    	/**
    	 * 存放所有需要校验验证码的url
    	 */
    	private Map<String, ValidateCodeType> urlMap = new HashMap<>();
    	/**
    	 * 验证请求url与配置的url是否匹配的工具类
    	 */
    	private AntPathMatcher pathMatcher = new AntPathMatcher();
    
    	/**
    	 * 初始化要拦截的url配置信息
    	 */
    	@Override
    	public void afterPropertiesSet() throws ServletException {
    		super.afterPropertiesSet();
    
    		urlMap.put(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM, ValidateCodeType.IMAGE);
    		addUrlToMap(securityProperties.getCode().getImage().getUrl(), ValidateCodeType.IMAGE);
    
    		urlMap.put(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, ValidateCodeType.SMS);
    		addUrlToMap(securityProperties.getCode().getSms().getUrl(), ValidateCodeType.SMS);
    	}
    
    	/**
    	 * 讲系统中配置的需要校验验证码的URL根据校验的类型放入map
    	 * 
    	 * @param urlString
    	 * @param type
    	 */
    	protected void addUrlToMap(String urlString, ValidateCodeType type) {
    		if (StringUtils.isNotBlank(urlString)) {
    			String[] urls = StringUtils.splitByWholeSeparatorPreserveAllTokens(urlString, ",");
    			for (String url : urls) {
    				urlMap.put(url, type);
    			}
    		}
    	}
    
    	/*
    	 * (non-Javadoc)
    	 * 
    	 * @see
    	 * org.springframework.web.filter.OncePerRequestFilter#doFilterInternal(
    	 * javax.servlet.http.HttpServletRequest,
    	 * javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain)
    	 */
    	@Override
    	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
    			throws ServletException, IOException {
    
    		ValidateCodeType type = getValidateCodeType(request);
    		if (type != null) {
    			logger.info("校验请求(" + request.getRequestURI() + ")中的验证码,验证码类型" + type);
    			try {
    				validateCodeProcessorHolder.findValidateCodeProcessor(type)
    						.validate(new ServletWebRequest(request, response));
    				logger.info("验证码校验通过");
    			} catch (ValidateCodeException exception) {
    				authenticationFailureHandler.onAuthenticationFailure(request, response, exception);
    				return;
    			}
    		}
    
    		chain.doFilter(request, response);
    
    	}
    
    	/**
    	 * 获取校验码的类型,如果当前请求不需要校验,则返回null
    	 * 
    	 * @param request
    	 * @return
    	 */
    	private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
    		ValidateCodeType result = null;
    		if (!StringUtils.equalsIgnoreCase(request.getMethod(), "get")) {
    			Set<String> urls = urlMap.keySet();
    			for (String url : urls) {
    				if (pathMatcher.match(url, request.getRequestURI())) {
    					result = urlMap.get(url);
    				}
    			}
    		}
    		return result;
    	}
    
    }  

    新配置:

            @Override
    	protected void configure(HttpSecurity http) throws Exception {
    		
                    ValidCodeFilter validCodeFilter = new ValidCodeFilter();
                    validCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
    
    		http.addFilterBefore(validCodeFilter, UsernamePasswordAuthenticationFilter.class)
                            .formLogin()
                            .loginPage("/imooc-signIn.html")
                            .loginProcessingUrl("/authentication/form")
                            .successHandler(imoocAuthenticationSuccessHandler)
                            .failureHandler(imoocAuthenticationFailureHandler)
    			.and()
    			.authorizeRequests()
                            .antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage()).permitAll()
    			.anyRequest()
    			.authenticated()
                            .and()
                            .csrf().disable();
    		
    	}
    

      

    6、重构图形验证码接口(视频4-8)

    重构内容:验证码基本参数可配置、验证码拦截接口可配置、验证码的生成逻辑可配置

    7、添加“记住我”功能

    “记住我”功能基本原理、“记住我”功能具体实现、“记住我”功能SpringSecurity源码解析

    配置:

    8、实现短信验证码登录(视频4-10 ~ 4-13)

    开发短信验证码接口、校验短信验证码并登录、重构代码

     登录页面:

    <h3>短信登录</h3>
    	<form action="/authentication/mobile" method="post">
    		<table>
    			<tr>
    				<td>手机号:</td>
    				<td><input type="text" name="mobile" value="13012345678"></td>
    			</tr>
    			<tr>
    				<td>短信验证码:</td>
    				<td>
    					<input type="text" name="smsCode">
    					<a href="/code/sms?mobile=13012345678">发送验证码</a>
    				</td>
    			</tr>
    			<tr>
    				<td colspan="2"><button type="submit">登录</button></td>
    			</tr>
    		</table>
    	</form>
    

      短信验证码生成接口略。

    短信登录开发需要写一个SmsAuthenticationFilter和SmsAuthenticationProvider

    关键类:

  • 相关阅读:
    win7环境下一次浅谈栈溢出
    字符约束条件的SQL注入攻击
    较有意思的Apple XSS(CVE-2016-7762)漏洞
    metasploit下Windows下多种提权方式
    NTFS ADS带来的web安全问题
    某安全团队内部渗透比赛
    MySQL注射绕过技巧(三)
    MySQL注射绕过技巧(二)
    MySQL注射绕过技巧
    centos7 zabbix3.4.6显示中文乱码问题
  • 原文地址:https://www.cnblogs.com/z-y-x/p/10428055.html
Copyright © 2020-2023  润新知