Spring Security Session管理
This commit is contained in:
parent
8dce7da513
commit
7e7b21c428
|
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>cc.mrbird</groupId>
|
||||
<artifactId>Security</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Security</name>
|
||||
<description>Demo project for Spring Boot</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>1.5.14.RELEASE</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.social</groupId>
|
||||
<artifactId>spring-social-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.session</groupId>
|
||||
<artifactId>spring-session</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
</project>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package cc.mrbird.security.browser;
|
||||
|
||||
import cc.mrbird.handler.MyAuthenticationFailureHandler;
|
||||
import cc.mrbird.handler.MyAuthenticationSucessHandler;
|
||||
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;
|
||||
|
||||
@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").permitAll() // 无需认证的请求路径
|
||||
.anyRequest() // 所有请求
|
||||
.authenticated() // 都需要认证
|
||||
.and()
|
||||
.sessionManagement() // 添加 Session管理器
|
||||
.invalidSessionUrl("/session/invalid") // Session失效后跳转到这个链接
|
||||
.maximumSessions(1)
|
||||
.maxSessionsPreventsLogin(true)
|
||||
.expiredSessionStrategy(sessionExpiredStrategy)
|
||||
.and()
|
||||
.and()
|
||||
.csrf().disable()
|
||||
.apply(smsAuthenticationConfig); // 将短信验证码认证配置加到 Spring Security 中
|
||||
}
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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("您的账号已经在别的地方登录,当前登录已失效。如果密码遭到泄露,请立即修改密码!");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<DefaultSecurityFilterChain, HttpSecurity> {
|
||||
|
||||
@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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<? extends GrantedAuthority> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
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已失效,请重新认证";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
security:
|
||||
basic:
|
||||
enabled: true
|
||||
|
||||
server:
|
||||
session:
|
||||
timeout: 3600
|
||||
|
||||
spring:
|
||||
session:
|
||||
store-type: redis
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>登录</title>
|
||||
<link rel="stylesheet" href="css/login.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<form class="login-page" action="/login" method="post">
|
||||
<div class="form">
|
||||
<h3>账户登录</h3>
|
||||
<input type="text" placeholder="用户名" name="username" required="required"/>
|
||||
<input type="password" placeholder="密码" name="password" required="required"/>
|
||||
<span style="display: inline">
|
||||
<input type="text" name="imageCode" placeholder="验证码" style="width: 50%;"/>
|
||||
<img src="/code/image"/>
|
||||
</span>
|
||||
<button type="submit">登录</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form class="login-page" action="/login/mobile" method="post">
|
||||
<div class="form">
|
||||
<h3>短信验证码登录</h3>
|
||||
<input type="text" placeholder="手机号" name="mobile" value="17777777777" required="required"/>
|
||||
<span style="display: inline">
|
||||
<input type="text" name="smsCode" placeholder="短信验证码" style="width: 50%;"/>
|
||||
<a href="/code/sms?mobile=17777777777">发送验证码</a>
|
||||
</span>
|
||||
<button type="submit">登录</button>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue