From cd018179c1b2d3c5568531af31b4ff8045afd30c Mon Sep 17 00:00:00 2001 From: mrbird <852252810@qq.com> Date: Tue, 18 Jun 2019 16:44:52 +0800 Subject: [PATCH] =?UTF-8?q?Spring=20Security=E9=80=80=E5=87=BA=E7=99=BB?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 60.Spring-Security-Logout/pom.xml | 65 ++++++++++++ .../java/cc/mrbird/SecurityApplication.java | 12 +++ .../main/java/cc/mrbird/domain/MyUser.java | 67 ++++++++++++ .../MyAuthenticationFailureHandler.java | 29 +++++ .../MyAuthenticationSucessHandler.java | 40 +++++++ .../handler/MyLogOutSuccessHandler.java | 25 +++++ .../browser/BrowserSecurityConfig.java | 81 ++++++++++++++ .../security/browser/UserDetailService.java | 32 ++++++ .../session/MySessionExpiredStrategy.java | 25 +++++ .../cc/mrbird/validate/code/ImageCode.java | 55 ++++++++++ .../validate/code/ValidateCodeException.java | 11 ++ .../validate/code/ValidateCodeFilter.java | 64 +++++++++++ .../smscode/SmsAuthenticationConfig.java | 40 +++++++ .../smscode/SmsAuthenticationFilter.java | 69 ++++++++++++ .../smscode/SmsAuthenticationProvider.java | 41 +++++++ .../smscode/SmsAuthenticationToken.java | 49 +++++++++ .../cc/mrbird/validate/smscode/SmsCode.java | 40 +++++++ .../validate/smscode/SmsCodeFilter.java | 66 ++++++++++++ .../controller/BrowserSecurityController.java | 50 +++++++++ .../mrbird/web/controller/TestController.java | 20 ++++ .../web/controller/ValidateController.java | 100 ++++++++++++++++++ .../src/main/resources/application.yml | 11 ++ .../main/resources/resources/css/login.css | 97 +++++++++++++++++ .../src/main/resources/resources/login.html | 34 ++++++ 24 files changed, 1123 insertions(+) create mode 100644 60.Spring-Security-Logout/pom.xml create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/SecurityApplication.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/domain/MyUser.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/handler/MyAuthenticationFailureHandler.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/handler/MyAuthenticationSucessHandler.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/handler/MyLogOutSuccessHandler.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/security/browser/BrowserSecurityConfig.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/security/browser/UserDetailService.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/session/MySessionExpiredStrategy.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/code/ImageCode.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/code/ValidateCodeException.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/code/ValidateCodeFilter.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationConfig.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationFilter.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationProvider.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationToken.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsCode.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsCodeFilter.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/web/controller/BrowserSecurityController.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/web/controller/TestController.java create mode 100644 60.Spring-Security-Logout/src/main/java/cc/mrbird/web/controller/ValidateController.java create mode 100644 60.Spring-Security-Logout/src/main/resources/application.yml create mode 100644 60.Spring-Security-Logout/src/main/resources/resources/css/login.css create mode 100644 60.Spring-Security-Logout/src/main/resources/resources/login.html diff --git a/60.Spring-Security-Logout/pom.xml b/60.Spring-Security-Logout/pom.xml new file mode 100644 index 00000000..339bcb24 --- /dev/null +++ b/60.Spring-Security-Logout/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + cc.mrbird + Security + 1.0-SNAPSHOT + jar + + Security + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 1.5.14.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.social + spring-social-config + + + org.apache.commons + commons-lang3 + 3.7 + + + org.springframework.session + spring-session + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/SecurityApplication.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/SecurityApplication.java new file mode 100644 index 00000000..47409349 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/SecurityApplication.java @@ -0,0 +1,12 @@ +package cc.mrbird; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SecurityApplication { + + public static void main(String[] args) { + SpringApplication.run(SecurityApplication.class, args); + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/domain/MyUser.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/domain/MyUser.java new file mode 100644 index 00000000..dee4f7ff --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/domain/MyUser.java @@ -0,0 +1,67 @@ +package cc.mrbird.domain; + +import java.io.Serializable; + +public class MyUser implements Serializable { + private static final long serialVersionUID = 3497935890426858541L; + + private String userName; + + private String password; + + private boolean accountNonExpired = true; + + private boolean accountNonLocked= true; + + private boolean credentialsNonExpired= true; + + private boolean enabled= true; + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isAccountNonExpired() { + return accountNonExpired; + } + + public void setAccountNonExpired(boolean accountNonExpired) { + this.accountNonExpired = accountNonExpired; + } + + public boolean isAccountNonLocked() { + return accountNonLocked; + } + + public void setAccountNonLocked(boolean accountNonLocked) { + this.accountNonLocked = accountNonLocked; + } + + public boolean isCredentialsNonExpired() { + return credentialsNonExpired; + } + + public void setCredentialsNonExpired(boolean credentialsNonExpired) { + this.credentialsNonExpired = credentialsNonExpired; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/handler/MyAuthenticationFailureHandler.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/handler/MyAuthenticationFailureHandler.java new file mode 100644 index 00000000..22127b47 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/handler/MyAuthenticationFailureHandler.java @@ -0,0 +1,29 @@ +package cc.mrbird.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { + + @Autowired + private ObjectMapper mapper; + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException { + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + response.setContentType("application/json;charset=utf-8"); + response.getWriter().write(mapper.writeValueAsString(exception.getMessage())); + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/handler/MyAuthenticationSucessHandler.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/handler/MyAuthenticationSucessHandler.java new file mode 100644 index 00000000..8dc29d39 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/handler/MyAuthenticationSucessHandler.java @@ -0,0 +1,40 @@ +package cc.mrbird.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.DefaultRedirectStrategy; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.security.web.savedrequest.SavedRequest; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler { + + // private RequestCache requestCache = new HttpSessionRequestCache(); + + private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + // + // @Autowired + // private ObjectMapper mapper; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException { + // response.setContentType("application/json;charset=utf-8"); + // response.getWriter().write(mapper.writeValueAsString(authentication)); + // SavedRequest savedRequest = requestCache.getRequest(request, response); + // System.out.println(savedRequest.getRedirectUrl()); + // redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl()); + redirectStrategy.sendRedirect(request, response, "/index"); + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/handler/MyLogOutSuccessHandler.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/handler/MyLogOutSuccessHandler.java new file mode 100644 index 00000000..3835aca7 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/handler/MyLogOutSuccessHandler.java @@ -0,0 +1,25 @@ +package cc.mrbird.handler; + +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author MrBird + */ +@Component +public class MyLogOutSuccessHandler implements LogoutSuccessHandler { + + @Override + public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { + httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); + httpServletResponse.setContentType("application/json;charset=utf-8"); + httpServletResponse.getWriter().write("退出成功,请重新登录"); + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/security/browser/BrowserSecurityConfig.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/security/browser/BrowserSecurityConfig.java new file mode 100644 index 00000000..a7e7483b --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/security/browser/BrowserSecurityConfig.java @@ -0,0 +1,81 @@ +package cc.mrbird.security.browser; + +import cc.mrbird.handler.MyAuthenticationFailureHandler; +import cc.mrbird.handler.MyAuthenticationSucessHandler; +import cc.mrbird.handler.MyLogOutSuccessHandler; +import cc.mrbird.session.MySessionExpiredStrategy; +import cc.mrbird.validate.code.ValidateCodeFilter; +import cc.mrbird.validate.smscode.SmsAuthenticationConfig; +import cc.mrbird.validate.smscode.SmsCodeFilter; +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.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private MyAuthenticationSucessHandler authenticationSucessHandler; + + @Autowired + private MyAuthenticationFailureHandler authenticationFailureHandler; + + @Autowired + private ValidateCodeFilter validateCodeFilter; + + @Autowired + private SmsCodeFilter smsCodeFilter; + + @Autowired + private SmsAuthenticationConfig smsAuthenticationConfig; + @Autowired + private MySessionExpiredStrategy sessionExpiredStrategy; + + @Autowired + private MyLogOutSuccessHandler logOutSuccessHandler; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + + http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加验证码校验过滤器 + .addFilterBefore(smsCodeFilter,UsernamePasswordAuthenticationFilter.class) // 添加短信验证码校验过滤器 + .formLogin() // 表单登录 + // http.httpBasic() // HTTP Basic + .loginPage("/authentication/require") // 登录跳转 URL + .loginProcessingUrl("/login") // 处理表单登录 URL + .successHandler(authenticationSucessHandler) // 处理登录成功 + .failureHandler(authenticationFailureHandler) // 处理登录失败 + .and() + .authorizeRequests() // 授权配置 + .antMatchers("/authentication/require", + "/login.html", "/code/image","/code/sms","/session/invalid", "/signout/success").permitAll() // 无需认证的请求路径 + .anyRequest() // 所有请求 + .authenticated() // 都需要认证 + .and() + .sessionManagement() // 添加 Session管理器 + .invalidSessionUrl("/session/invalid") // Session失效后跳转到这个链接 + .maximumSessions(1) + .maxSessionsPreventsLogin(true) + .expiredSessionStrategy(sessionExpiredStrategy) + .and() + .and() + .logout() + .logoutUrl("/signout") + // .logoutSuccessUrl("/signout/success") + .logoutSuccessHandler(logOutSuccessHandler) + .deleteCookies("JSESSIONID") + .and() + .csrf().disable() + .apply(smsAuthenticationConfig); // 将短信验证码认证配置加到 Spring Security 中 + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/security/browser/UserDetailService.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/security/browser/UserDetailService.java new file mode 100644 index 00000000..58992aa1 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/security/browser/UserDetailService.java @@ -0,0 +1,32 @@ +package cc.mrbird.security.browser; + +import cc.mrbird.domain.MyUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +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; + +@Configuration +public class UserDetailService implements UserDetailsService { + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 模拟一个用户,替代数据库获取逻辑 + MyUser user = new MyUser(); + user.setUserName(username); + user.setPassword(this.passwordEncoder.encode("123456")); + // 输出加密后的密码 + System.out.println(user.getPassword()); + + return new User(username, user.getPassword(), user.isEnabled(), + user.isAccountNonExpired(), user.isCredentialsNonExpired(), + user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/session/MySessionExpiredStrategy.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/session/MySessionExpiredStrategy.java new file mode 100644 index 00000000..3d97bc54 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/session/MySessionExpiredStrategy.java @@ -0,0 +1,25 @@ +package cc.mrbird.session; + +import org.springframework.http.HttpStatus; +import org.springframework.security.web.session.SessionInformationExpiredEvent; +import org.springframework.security.web.session.SessionInformationExpiredStrategy; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author MrBird + */ +@Component +public class MySessionExpiredStrategy implements SessionInformationExpiredStrategy { + + @Override + public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { + HttpServletResponse response = event.getResponse(); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType("application/json;charset=utf-8"); + response.getWriter().write("您的账号已经在别的地方登录,当前登录已失效。如果密码遭到泄露,请立即修改密码!"); + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/code/ImageCode.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/code/ImageCode.java new file mode 100644 index 00000000..bd6fd377 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/code/ImageCode.java @@ -0,0 +1,55 @@ +package cc.mrbird.validate.code; + +import java.awt.image.BufferedImage; +import java.io.Serializable; +import java.time.LocalDateTime; + +public class ImageCode implements Serializable { + + private static final long serialVersionUID = -7831615057416168810L; + 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; + } + + boolean isExpire() { + return LocalDateTime.now().isAfter(expireTime); + } + + public BufferedImage getImage() { + return image; + } + + public void setImage(BufferedImage image) { + this.image = image; + } + + 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; + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/code/ValidateCodeException.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/code/ValidateCodeException.java new file mode 100644 index 00000000..f958f665 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/code/ValidateCodeException.java @@ -0,0 +1,11 @@ +package cc.mrbird.validate.code; + +import org.springframework.security.core.AuthenticationException; + +public class ValidateCodeException extends AuthenticationException { + private static final long serialVersionUID = 5022575393500654458L; + + public ValidateCodeException(String message) { + super(message); + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/code/ValidateCodeFilter.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/code/ValidateCodeFilter.java new file mode 100644 index 00000000..dcf557a0 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/code/ValidateCodeFilter.java @@ -0,0 +1,64 @@ +package cc.mrbird.validate.code; + +import cc.mrbird.web.controller.ValidateController; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.social.connect.web.HttpSessionSessionStrategy; +import org.springframework.social.connect.web.SessionStrategy; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class ValidateCodeFilter extends OncePerRequestFilter { + + @Autowired + private AuthenticationFailureHandler authenticationFailureHandler; + + private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { + if (StringUtils.equalsIgnoreCase("/login", httpServletRequest.getRequestURI()) + && StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "post")) { + try { + validateCode(new ServletWebRequest(httpServletRequest)); + } catch (ValidateCodeException e) { + authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e); + return; + } + } + filterChain.doFilter(httpServletRequest, httpServletResponse); + } + + private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException { + ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE); + String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode"); + + if (StringUtils.isBlank(codeInRequest)) { + throw new ValidateCodeException("验证码不能为空!"); + } + if (codeInSession == null) { + throw new ValidateCodeException("验证码不存在!"); + } + if (codeInSession.isExpire()) { + sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE); + throw new ValidateCodeException("验证码已过期!"); + } + if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(), codeInRequest)) { + throw new ValidateCodeException("验证码不正确!"); + } + sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE); + + } + +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationConfig.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationConfig.java new file mode 100644 index 00000000..b76d9316 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationConfig.java @@ -0,0 +1,40 @@ +package cc.mrbird.validate.smscode; + +import cc.mrbird.security.browser.UserDetailService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.stereotype.Component; + +@Component +public class SmsAuthenticationConfig extends SecurityConfigurerAdapter { + + @Autowired + private AuthenticationSuccessHandler authenticationSuccessHandler; + + @Autowired + private AuthenticationFailureHandler authenticationFailureHandler; + + @Autowired + private UserDetailService userDetailService; + + @Override + public void configure(HttpSecurity http) throws Exception { + SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter(); + smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); + smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler); + smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler); + + SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider(); + smsAuthenticationProvider.setUserDetailService(userDetailService); + + http.authenticationProvider(smsAuthenticationProvider) + .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationFilter.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationFilter.java new file mode 100644 index 00000000..249f3bef --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationFilter.java @@ -0,0 +1,69 @@ +package cc.mrbird.validate.smscode; + +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + + public static final String MOBILE_KEY = "mobile"; + + private String mobileParameter = MOBILE_KEY; + private boolean postOnly = true; + + + public SmsAuthenticationFilter() { + super(new AntPathRequestMatcher("/login/mobile", "POST")); + } + + + public Authentication attemptAuthentication(HttpServletRequest request, + HttpServletResponse response) throws AuthenticationException { + if (postOnly && !request.getMethod().equals("POST")) { + throw new AuthenticationServiceException( + "Authentication method not supported: " + request.getMethod()); + } + + String mobile = obtainMobile(request); + + if (mobile == null) { + mobile = ""; + } + + mobile = mobile.trim(); + + SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile); + + setDetails(request, authRequest); + + return this.getAuthenticationManager().authenticate(authRequest); + } + + protected String obtainMobile(HttpServletRequest request) { + return request.getParameter(mobileParameter); + } + + protected void setDetails(HttpServletRequest request, + SmsAuthenticationToken authRequest) { + authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); + } + + public void setMobileParameter(String mobileParameter) { + Assert.hasText(mobileParameter, "mobile parameter must not be empty or null"); + this.mobileParameter = mobileParameter; + } + + public void setPostOnly(boolean postOnly) { + this.postOnly = postOnly; + } + + public final String getMobileParameter() { + return mobileParameter; + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationProvider.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationProvider.java new file mode 100644 index 00000000..e15b04cd --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationProvider.java @@ -0,0 +1,41 @@ +package cc.mrbird.validate.smscode; + +import cc.mrbird.security.browser.UserDetailService; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; + +public class SmsAuthenticationProvider implements AuthenticationProvider { + + private UserDetailService userDetailService; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication; + UserDetails userDetails = userDetailService.loadUserByUsername((String) authenticationToken.getPrincipal()); + + if (userDetails == null) + throw new InternalAuthenticationServiceException("未找到与该手机号对应的用户"); + + SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities()); + + authenticationResult.setDetails(authenticationToken.getDetails()); + + return authenticationResult; + } + + @Override + public boolean supports(Class aClass) { + return SmsAuthenticationToken.class.isAssignableFrom(aClass); + } + + public UserDetailService getUserDetailService() { + return userDetailService; + } + + public void setUserDetailService(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationToken.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationToken.java new file mode 100644 index 00000000..f33670c7 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsAuthenticationToken.java @@ -0,0 +1,49 @@ +package cc.mrbird.validate.smscode; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; + +import java.util.Collection; + +public class SmsAuthenticationToken extends AbstractAuthenticationToken { + + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + private final Object principal; + + public SmsAuthenticationToken(String mobile) { + super(null); + this.principal = mobile; + setAuthenticated(false); + } + + public SmsAuthenticationToken(Object principal, Collection authorities) { + super(authorities); + this.principal = principal; + super.setAuthenticated(true); // must use super, as we override + } + + @Override + public Object getCredentials() { + return null; + } + + public Object getPrincipal() { + return this.principal; + } + + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + if (isAuthenticated) { + throw new IllegalArgumentException( + "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); + } + + super.setAuthenticated(false); + } + + @Override + public void eraseCredentials() { + super.eraseCredentials(); + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsCode.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsCode.java new file mode 100644 index 00000000..537135e0 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsCode.java @@ -0,0 +1,40 @@ +package cc.mrbird.validate.smscode; + +import java.time.LocalDateTime; + +public class SmsCode { + + private String code; + + private LocalDateTime expireTime; + + public SmsCode(String code, int expireIn) { + this.code = code; + this.expireTime = LocalDateTime.now().plusSeconds(expireIn); + } + + public SmsCode(String code, LocalDateTime expireTime) { + this.code = code; + this.expireTime = expireTime; + } + + boolean isExpire() { + 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; + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsCodeFilter.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsCodeFilter.java new file mode 100644 index 00000000..c7f64993 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/validate/smscode/SmsCodeFilter.java @@ -0,0 +1,66 @@ +package cc.mrbird.validate.smscode; + +import cc.mrbird.validate.code.ValidateCodeException; +import cc.mrbird.web.controller.ValidateController; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.social.connect.web.HttpSessionSessionStrategy; +import org.springframework.social.connect.web.SessionStrategy; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class SmsCodeFilter extends OncePerRequestFilter { + + @Autowired + private AuthenticationFailureHandler authenticationFailureHandler; + + private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { + if (StringUtils.equalsIgnoreCase("/login/mobile", httpServletRequest.getRequestURI()) + && StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "post")) { + try { + validateCode(new ServletWebRequest(httpServletRequest)); + } catch (ValidateCodeException e) { + authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e); + return; + } + } + filterChain.doFilter(httpServletRequest, httpServletResponse); + } + + private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException { + String smsCodeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "smsCode"); + String mobileInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "smsCode"); + + SmsCode codeInSession = (SmsCode) sessionStrategy.getAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobileInRequest); + + if (StringUtils.isBlank(smsCodeInRequest)) { + throw new ValidateCodeException("验证码不能为空!"); + } + if (codeInSession == null) { + throw new ValidateCodeException("验证码不存在!"); + } + if (codeInSession.isExpire()) { + sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE); + throw new ValidateCodeException("验证码已过期!"); + } + if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(), smsCodeInRequest)) { + throw new ValidateCodeException("验证码不正确!"); + } + sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE); + + } +} \ No newline at end of file diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/web/controller/BrowserSecurityController.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/web/controller/BrowserSecurityController.java new file mode 100644 index 00000000..a26fe88b --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/web/controller/BrowserSecurityController.java @@ -0,0 +1,50 @@ +package cc.mrbird.web.controller; + +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.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +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; + +/** + * @author MrBird + */ +@RestController +public class BrowserSecurityController { + + private RequestCache requestCache = new HttpSessionRequestCache(); + + private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + + @GetMapping("/authentication/require") + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public String requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException { + SavedRequest savedRequest = requestCache.getRequest(request, response); + if (savedRequest != null) { + String targetUrl = savedRequest.getRedirectUrl(); + if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) + redirectStrategy.sendRedirect(request, response, "/login.html"); + } + return "访问的资源需要身份认证!"; + } + + @GetMapping("/session/invalid") + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public String sessionInvalid() { + return "session已失效,请重新认证"; + } + + @GetMapping("/signout/success") + public String signout() { + return "退出成功,请重新登录"; + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/web/controller/TestController.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/web/controller/TestController.java new file mode 100644 index 00000000..b677e616 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/web/controller/TestController.java @@ -0,0 +1,20 @@ +package cc.mrbird.web.controller; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + @GetMapping("hello") + public String hello() { + return "hello spring security"; + } + + @GetMapping("index") + public Object index(Authentication authentication) { + // return SecurityContextHolder.getContext().getAuthentication(); + return authentication; + } +} diff --git a/60.Spring-Security-Logout/src/main/java/cc/mrbird/web/controller/ValidateController.java b/60.Spring-Security-Logout/src/main/java/cc/mrbird/web/controller/ValidateController.java new file mode 100644 index 00000000..f43fe738 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/java/cc/mrbird/web/controller/ValidateController.java @@ -0,0 +1,100 @@ +package cc.mrbird.web.controller; + +import cc.mrbird.validate.code.ImageCode; +import cc.mrbird.validate.smscode.SmsCode; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.social.connect.web.HttpSessionSessionStrategy; +import org.springframework.social.connect.web.SessionStrategy; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.ServletWebRequest; + +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.Random; + +@RestController +public class ValidateController { + + public final static String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE"; + + public final static String SESSION_KEY_SMS_CODE = "SESSION_KEY_SMS_CODE"; + + private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); + + @GetMapping("/code/image") + public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException { + ImageCode imageCode = createImageCode(); + ImageCode codeInRedis = new ImageCode(null,imageCode.getCode(),imageCode.getExpireTime()); + sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, codeInRedis); + ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream()); + } + + @GetMapping("/code/sms") + public void createSmsCode(HttpServletRequest request, HttpServletResponse response, String mobile) { + SmsCode smsCode = createSMSCode(); + sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_SMS_CODE + mobile, smsCode); + // 输出验证码到控制台代替短信发送服务 + System.out.println("您的登录验证码为:" + smsCode.getCode() + ",有效时间为60秒"); + } + + private SmsCode createSMSCode() { + String code = RandomStringUtils.randomNumeric(6); + return new SmsCode(code, 60); + } + + private ImageCode createImageCode() { + int width = 100; // 验证码图片宽度 + int height = 36; // 验证码图片长度 + int length = 4; // 验证码位数 + int expireIn = 60; // 验证码有效时间 60s + + 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); + } + + 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(), expireIn); + } + + 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); + } + +} diff --git a/60.Spring-Security-Logout/src/main/resources/application.yml b/60.Spring-Security-Logout/src/main/resources/application.yml new file mode 100644 index 00000000..e98b1648 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/resources/application.yml @@ -0,0 +1,11 @@ +security: + basic: + enabled: true + +server: + session: + timeout: 3600 + +spring: + session: + store-type: redis \ No newline at end of file diff --git a/60.Spring-Security-Logout/src/main/resources/resources/css/login.css b/60.Spring-Security-Logout/src/main/resources/resources/css/login.css new file mode 100644 index 00000000..52d16991 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/resources/resources/css/login.css @@ -0,0 +1,97 @@ +.login-page { + width: 360px; + padding: 8% 0 0; + margin: auto; +} +.form { + position: relative; + z-index: 1; + background: #ffffff; + max-width: 360px; + margin: 0 auto 100px; + padding: 45px; + text-align: center; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24); +} +.form input { + outline: 0; + background: #f2f2f2; + width: 100%; + border: 0; + margin: 0 0 15px; + padding: 15px; + box-sizing: border-box; + font-size: 14px; +} +.form button { + text-transform: uppercase; + outline: 0; + background: #4caf50; + width: 100%; + border: 0; + padding: 15px; + color: #ffffff; + font-size: 14px; + -webkit-transition: all 0.3 ease; + transition: all 0.3 ease; + cursor: pointer; +} +.form button:hover, +.form button:active, +.form button:focus { + background: #43a047; +} +.form .message { + margin: 15px 0 0; + color: #b3b3b3; + font-size: 12px; +} +.form .message a { + color: #4caf50; + text-decoration: none; +} +.form .register-form { + display: none; +} +.container { + position: relative; + z-index: 1; + max-width: 300px; + margin: 0 auto; +} +.container:before, +.container:after { + content: ""; + display: block; + clear: both; +} +.container .info { + margin: 50px auto; + text-align: center; +} +.container .info h1 { + margin: 0 0 15px; + padding: 0; + font-size: 36px; + font-weight: 300; + color: #1a1a1a; +} +.container .info span { + color: #4d4d4d; + font-size: 12px; +} +.container .info span a { + color: #000000; + text-decoration: none; +} +.container .info span .fa { + color: #ef3b3a; +} +body { + background: #76b852; /* fallback for old browsers */ + background: -webkit-linear-gradient(right, #76b852, #8dc26f); + background: -moz-linear-gradient(right, #76b852, #8dc26f); + background: -o-linear-gradient(right, #76b852, #8dc26f); + background: linear-gradient(to left, #76b852, #8dc26f); + font-family: Lato,"PingFang SC","Microsoft YaHei",sans-serif; +} diff --git a/60.Spring-Security-Logout/src/main/resources/resources/login.html b/60.Spring-Security-Logout/src/main/resources/resources/login.html new file mode 100644 index 00000000..13a9cdc9 --- /dev/null +++ b/60.Spring-Security-Logout/src/main/resources/resources/login.html @@ -0,0 +1,34 @@ + + + + + 登录 + + + +
+
+

账户登录

+ + + + + + + +
+
+ +
+
+

短信验证码登录

+ + + + 发送验证码 + + +
+
+ + \ No newline at end of file