SpringSecurity基本原理
application.yml注释掉关闭security的配置
#先关闭security默认配置
#security:
# basic:
# enabled: false
创建配置文件
image.png表单登陆方式
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //表单登陆页
//http.httpBasic() //security默认方式
.and()
.authorizeRequests()//下面授权配置
.anyRequest()//所有请求
.authenticated();//都需要身份认证
}
}
image.png
账号是user密码是程序启动时生成的密码
默认登陆方式
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.formLogin() //表单登陆页
http.httpBasic() //security默认方式
.and()
.authorizeRequests()//下面授权配置
.anyRequest()//所有请求
.authenticated();//都需要身份认证
}
}
image.png
过滤器链
自定义认证用户逻辑
打开密码加密
image.png
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//密码加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //表单登陆页
//http.httpBasic() //security默认方式
.and()
.authorizeRequests()//下面授权配置
.anyRequest()//所有请求
.authenticated();//都需要身份认证
}
}
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
//认证登陆
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//isAccountNonExpired 账号是否过期
//isCredentialsNonExpired 密码是否过期
//isAccountNonLocked 账号是否锁定用于冻结状态
//isEnabled 账号是否可用用于删除状态
//第一个参数是传进来的账号,第二个参数是数据库根据账号查询到到密码
//第三个参数是账号是否可用,第四个参数是账号是否过期
//第五个参数是密码是否过期,第六个参数账号是否锁定
//第七个参数账号到权限
return new User(username,userRepository.findByUsername(username).getPassword(),
true,true,true,userRepository.findByUsername(username).getIsAccountNonLocked(),
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
//模拟注册
public void register(){
com.guosh.security.browser.domain.User user=new com.guosh.security.browser.domain.User();
user.setUsername("ceshi");
//存入加密后的密码
user.setPassword(passwordEncoder.encode("123456"));
user.setIsEnabled(true);
user.setIsAccountNonLocked(true);
userRepository.save(user);
}
}
个性化用户认证流程
自定义登陆页面并实现form表单提交
image.png创建登陆页面
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Login Form</h1>
<form action="/guoshsecurity/authentication/form" method="post">
<input type="text" class="text" value="" name="username">
<input type="password" value="" name="password">
<input type="submit" value="Login" >
</form>
</body>
</html>
添加登陆页面地址,并把登陆页面地址设置不需要认证,自定义form表单提交地址
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//密码加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()//关闭跨站防护
.authorizeRequests()//下面授权配置
.antMatchers("/defaultLogin.html").permitAll()//login请求除外不需要认证
.anyRequest().authenticated()//所有请求都需要身份认证
.and()
.formLogin() //表单登陆页
.loginPage("/defaultLogin.html")//登陆页面
.loginProcessingUrl("/authentication/form");//自定义form表单登陆提交地址默认是/login
}
}
处理不同类型的请求
image.png@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
//密码加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()//关闭跨站防护
.authorizeRequests()//下面授权配置
.antMatchers("/login",securityProperties.getBrowser().getLoginPage()).permitAll()//login请求除外不需要认证
.anyRequest().authenticated()//所有请求都需要身份认证
.and()
.formLogin() //表单登陆页
.loginPage("/login")//登陆页面
.loginProcessingUrl("/authentication/form");//自定义form表单登陆提交地址默认是/login
}
}
处理登陆请求是跳转到默认登陆页面或者用户自定义登陆页面
@RestController
public class BrowserSecurityController {
private RequestCache requestCache=new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy=new DefaultRedirectStrategy();
@Autowired
private SecurityProperties securityProperties;
/**
* 需要身份认证时跳转地址
* @param request
* @param response
* @return
*/
@RequestMapping("/login")
@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();
//System.out.println(savedRequest);
if(StringUtils.endsWithIgnoreCase(targetUrl,".html")){
redirectStrategy.sendRedirect(request,response,securityProperties.getBrowser().getLoginPage());
}
}
return new SimpleResponse("访问服务需要身份认证,请引导用户到登陆页");
}
}
自定义登陆页面
image.png@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}
public class BrowserProperties {
private String loginPage="/defaultLogin.html"; //如果用户没有配置登陆页面走默认
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
}
@ConfigurationProperties(prefix = "guosh.security")
public class SecurityProperties {
private BrowserProperties browser=new BrowserProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
}
用户如果在application.yml指定了登陆页面会跳转指定登陆页
#自定义登陆页面
guosh:
security:
browser:
loginPage: /demoLogin.html
自定义登陆成功处理
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private BrowserAuthenticationSuccessHandler browserAuthenticationSuccessHandler;
@Autowired
private BrowserAuthenticationFailureHandler browserAuthenticationFailureHandler;
//密码加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()//关闭跨站防护
.authorizeRequests()//下面授权配置
.antMatchers("/login",securityProperties.getBrowser().getLoginPage()).permitAll()//login请求除外不需要认证
.anyRequest().authenticated()//所有请求都需要身份认证
.and()
.formLogin() //表单登陆页
.loginPage("/login")//登陆页面
.loginProcessingUrl("/authentication/form")//自定义form表单登陆提交地址默认是/login
.successHandler(browserAuthenticationSuccessHandler)//自定义登陆成功后返回json信息
.failureHandler(browserAuthenticationFailureHandler);//自定义登陆失败返回json
}
}
创建自定义成功失败的实现方法
image.png
@Component
public class BrowserAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private Logger logger= LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
//自定义登陆成功
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
logger.info("登陆成功");
response.setContentType("application/json;charset=UTF-8");
//转json字符串返回
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
@Component
public class BrowserAuthenticationFailureHandler implements AuthenticationFailureHandler {
private Logger logger= LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
//自定义登陆失败
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
logger.info("登陆失败");
//401状态码
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
//转json字符串返回
response.getWriter().write(objectMapper.writeValueAsString(e));
}
}
自定义配置支持同步请求与异步请求
创建可配置的请求方式
image.png
public enum LoginType {
REDITECT,
JSON
}
public class BrowserProperties {
//自定义登陆页面
private String loginPage="/defaultLogin.html"; //如果用户没有配置登陆页面走默认
//自定义处理登陆请求默认异步
private LoginType loginType=LoginType.JSON;
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
public LoginType getLoginType() {
return loginType;
}
public void setLoginType(LoginType loginType) {
this.loginType = loginType;
}
}
创建自定义成功失败的实现方法
image.png
@Component
public class BrowserAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private Logger logger= LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
//自定义登陆成功
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
logger.info("登陆成功");
//判断异步请求还是同步请求
if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
response.setContentType("application/json;charset=UTF-8");
//转json字符串返回
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
else {
super.onAuthenticationSuccess(request,response,authentication);
}
}
}
@Component
public class BrowserAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private Logger logger= LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
//自定义登陆失败
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
logger.info("登陆失败");
if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
//401状态码
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
//转json字符串返回
response.getWriter().write(objectMapper.writeValueAsString(e));
}else{
super.onAuthenticationFailure(request,response,e);
}
}
}
用户如果在application.yml指定了请求方式会按照指定的方式执行
guosh:
security:
browser:
loginPage: /demoLogin.html
#JSON异步登陆 REDITECT同步登陆
loginType: JSON
获取登陆用户信息
@RequestMapping(value = "/me",method = RequestMethod.GET)
public Object getLoginUser() {
//获取详细登陆信息
//SecurityContextHolder.getContext().getAuthentication();
//获取用户的登陆信息
return ((UserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal());
}
图片验证码
图形验证码接口
image.png验证码实体类
public class ImageCode {
//验证码图片
private BufferedImage image;
//随机数
private String code;
//失效时间
private LocalDateTime exireTime;
public ImageCode(BufferedImage image, String code, int exireIn) {
this.image = image;
this.code = code;
//当前时间加上过期秒数
this.exireTime = LocalDateTime.now().plusSeconds(exireIn);
}
//判断时间是否过期
public boolean isExpried(){
return LocalDateTime.now().isAfter(exireTime);
}
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 getExireTime() {
return exireTime;
}
public void setExireTime(LocalDateTime exireTime) {
this.exireTime = exireTime;
}
}
验证码接口实现
@RestController
public class ValidateCodeController {
public static final String SESSION_KEY="SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
@RequestMapping(value = "/code/image",method = RequestMethod.GET)
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//生成验证码对象
ImageCode imageCode=caeateImageCode(request);
//将ImageCode对象保存在session中
sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
//返回到页面
ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
}
private ImageCode caeateImageCode(HttpServletRequest request) {
int width = 67;
int height = 23;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
String sRand = "";
for (int i = 0; i < 4; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(image, sRand, 60);
}
/**
* 生成随机背景条纹
*
* @param fc
* @param bc
* @return
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
验证码的异常处理器
public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg) {
super(msg);
}
}
在登陆之前验证验证码的拦截
public class ValidateCodeFilter extends OncePerRequestFilter {
//登陆失败的处理器
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//必须是登陆请求并且是post请求
if(StringUtils.equals("/guoshsecurity/authentication/form",request.getRequestURI())&&StringUtils.equalsIgnoreCase(request.getMethod(),"post")){
try {
//校验验证码
validate(new ServletWebRequest(request));
}catch (ValidateCodeException e){
authenticationFailureHandler.onAuthenticationFailure(request,response,e);
return;
}
}
filterChain.doFilter(request,response);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
//从session获取验证码
ImageCode codeInSession=(ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
//从请求里获取验证码
String codeInRequest=ServletRequestUtils.getStringParameter(request.getRequest(),"imageCode");
if(StringUtils.isBlank(codeInRequest)){
throw new ValidateCodeException("验证码不能为空");
}
if(codeInSession == null){
throw new ValidateCodeException("验证码不存在");
}
//判断验证码时间是否过期
if(codeInSession.isExpried()){
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("验证码已经过期");
}
//比对验证码
if(!StringUtils.equalsIgnoreCase(codeInSession.getCode(),codeInRequest)){
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
}
修改WebSecurityConfig配置文件
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private BrowserAuthenticationSuccessHandler browserAuthenticationSuccessHandler;
@Autowired
private BrowserAuthenticationFailureHandler browserAuthenticationFailureHandler;
@Autowired
private SessionRegistry sessionRegistry;
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
//密码加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter=new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(browserAuthenticationFailureHandler);
http
.csrf().disable()//关闭跨站防护
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)//在登陆拦截之前添加验证码拦截器
.authorizeRequests()//下面授权配置
.antMatchers("/login",securityProperties.getBrowser().getLoginPage(),"/code/image").permitAll()//login请求除外不需要认证
.anyRequest().authenticated()//所有请求都需要身份认证
.and()
.formLogin() //表单登陆页
.loginPage("/login")//登陆页面
.loginProcessingUrl("/authentication/form")//自定义form表单登陆提交地址默认是/login
.successHandler(browserAuthenticationSuccessHandler)//自定义登陆成功后返回json信息
.failureHandler(browserAuthenticationFailureHandler)//自定义登陆失败返回json
.and()
.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry).expiredUrl("/login");//用户只能登陆一次
}
}
图形验证码重构
验证码基本参数,验证码拦截接口可配置
image.pngpublic class ImageCodeProperties {
//默认验证码宽度
private int width = 67;
//默认验证码高度
private int height = 23;
//默认验证码位数
private int length = 4;
//默认验证码失效时间
private int expreIn= 60;
//哪些url地址需要验证码拦截
private String url;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public int getExpreIn() {
return expreIn;
}
public void setExpreIn(int expreIn) {
this.expreIn = expreIn;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
public class ValidateCodeProperties {
private ImageCodeProperties image=new ImageCodeProperties();
public ImageCodeProperties getImage() {
return image;
}
public void setImage(ImageCodeProperties image) {
this.image = image;
}
}
@ConfigurationProperties(prefix = "guosh.security")
public class SecurityProperties {
//浏览器登陆配置
private BrowserProperties browser=new BrowserProperties();
//验证码配置
private ValidateCodeProperties code=new ValidateCodeProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
public ValidateCodeProperties getCode() {
return code;
}
public void setCode(ValidateCodeProperties code) {
this.code = code;
}
}
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
//登陆失败的处理器
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
private Set<String>urls = new HashSet<>();
private SecurityProperties securityProperties;
private AntPathMatcher antPathMatcher=new AntPathMatcher();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
//分割转换数组
String[]configUrls=StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(),",");
for (String configUrl:configUrls) {
urls.add(configUrl);
}
urls.add("/authentication/form");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
//用antPathMatcher工具栏判断路径是否符合
for (String url:urls) {
if(antPathMatcher.match(url,request.getServletPath())){
action=true;
}
}
//必须是登陆请求并且是post请求
if(action){
try {
//校验验证码
validate(new ServletWebRequest(request));
}catch (ValidateCodeException e){
authenticationFailureHandler.onAuthenticationFailure(request,response,e);
return;
}
}
filterChain.doFilter(request,response);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
//从session获取验证码
ImageCode codeInSession=(ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
//从请求里获取验证码
String codeInRequest=ServletRequestUtils.getStringParameter(request.getRequest(),"imageCode");
if(StringUtils.isBlank(codeInRequest)){
throw new ValidateCodeException("验证码不能为空");
}
if(codeInSession == null){
throw new ValidateCodeException("验证码不存在");
}
//判断验证码时间是否过期
if(codeInSession.isExpried()){
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("验证码已经过期");
}
//比对验证码
if(!StringUtils.equalsIgnoreCase(codeInSession.getCode(),codeInRequest)){
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private BrowserAuthenticationSuccessHandler browserAuthenticationSuccessHandler;
@Autowired
private BrowserAuthenticationFailureHandler browserAuthenticationFailureHandler;
@Autowired
private SessionRegistry sessionRegistry;
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
//密码加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter=new ValidateCodeFilter();
//处理失败异常
validateCodeFilter.setAuthenticationFailureHandler(browserAuthenticationFailureHandler);
//把配置放进去
validateCodeFilter.setSecurityProperties(securityProperties);
//初始化方法
validateCodeFilter.afterPropertiesSet();
http
.csrf().disable()//关闭跨站防护
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)//在登陆拦截之前添加验证码拦截器
.authorizeRequests()//下面授权配置
.antMatchers("/login",securityProperties.getBrowser().getLoginPage(),"/code/image").permitAll()//login请求除外不需要认证
.anyRequest().authenticated()//所有请求都需要身份认证
.and()
.formLogin() //表单登陆页
.loginPage("/login")//登陆页面
.loginProcessingUrl("/authentication/form")//自定义form表单登陆提交地址默认是/login
.successHandler(browserAuthenticationSuccessHandler)//自定义登陆成功后返回json信息
.failureHandler(browserAuthenticationFailureHandler)//自定义登陆失败返回json
.and()
.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry).expiredUrl("/login");//用户只能登陆一次
}
}
配置文件
code:
#image下面的属性有width,height,length,expreIn,url
image:
length: 5
url: /user/*
验证码生成逻辑可配置
public interface ValidateCodeGenerator {
ImageCode generate(ServletWebRequest request);
}
public class ImageCodeGenerator implements ValidateCodeGenerator{
@Autowired
private SecurityProperties securityProperties;
@Override
public ImageCode generate(ServletWebRequest request) {
//先从请求中获取如果获取不到从配置文件获取
int width = ServletRequestUtils.getIntParameter(request.getRequest(),"width",securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request.getRequest(),"height",securityProperties.getCode().getImage().getHeight());
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
String sRand = "";
for (int i = 0; i < securityProperties.getCode().getImage().getLength(); i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpreIn());
}
/**
* 生成随机背景条纹
*
* @param fc
* @param bc
* @return
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
@Configuration
public class ValidateCodeBeanConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
@ConditionalOnMissingBean(name = "imageCodeGenerator")//在spring加载bean时会先查找有没有名字为imageCodeGenerator
public ValidateCodeGenerator imageCodeGenerator(){
ImageCodeGenerator codeGenerator=new ImageCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
}
@RestController
public class ValidateCodeController {
public static final String SESSION_KEY="SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
@Autowired
private ValidateCodeGenerator imageCodeGenerator;
@RequestMapping(value = "/code/image",method = RequestMethod.GET)
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//生成验证码对象
ImageCode imageCode=imageCodeGenerator.generate(new ServletWebRequest(request));
//将ImageCode对象保存在session中
sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
//返回到页面
ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
}
}
如果想创建自己的验证码生成逻辑
image.png
/**
* 从类可以重写生成验证码的逻辑替换默认逻辑
*/
@Component(value = "imageCodeGenerator")
public class demoImageCodeGenerator implements ValidateCodeGenerator {
@Override
public ImageCode generate(ServletWebRequest request) {
System.out.printf("自定义验证码");
return null;
}
}
记住我功能实现
基本原理
image.png前端登陆页面传值名称必须叫emember-me
<input type="checkbox" value="true" name="remember-me" >记住密码
配置可配置记住密码过期时间
public class BrowserProperties {
//自定义登陆页面
private String loginPage = "/defaultLogin.html"; //如果用户没有配置登陆页面走默认
//自定义处理登陆请求默认异步
private LoginType loginType = LoginType.JSON;
//设置记住密码过期时间默认1小时
private int rememberMeSeconds = 3600;
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
public LoginType getLoginType() {
return loginType;
}
public void setLoginType(LoginType loginType) {
this.loginType = loginType;
}
public int getRememberMeSeconds() {
return rememberMeSeconds;
}
public void setRememberMeSeconds(int rememberMeSeconds) {
this.rememberMeSeconds = rememberMeSeconds;
}
}
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//自定义配置文件
@Autowired
private SecurityProperties securityProperties;
//处理登陆成功
@Autowired
private BrowserAuthenticationSuccessHandler browserAuthenticationSuccessHandler;
//处理登陆失败
@Autowired
private BrowserAuthenticationFailureHandler browserAuthenticationFailureHandler;
//数据源
@Autowired
private DataSource dataSource;
//登陆实现
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SessionRegistry sessionRegistry;
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
//密码加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//记住密码
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//启动自动创建persistent_logins表
//tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//验证码拦截器
ValidateCodeFilter validateCodeFilter=new ValidateCodeFilter();
//处理失败异常
validateCodeFilter.setAuthenticationFailureHandler(browserAuthenticationFailureHandler);
//把配置放进去
validateCodeFilter.setSecurityProperties(securityProperties);
//初始化方法
validateCodeFilter.afterPropertiesSet();
http
.csrf().disable()//关闭跨站防护
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)//在登陆拦截之前添加验证码拦截器
.authorizeRequests()//下面授权配置
.antMatchers("/login",securityProperties.getBrowser().getLoginPage(),"/code/image").permitAll()//login请求除外不需要认证
.anyRequest().authenticated()//所有请求都需要身份认证
.and()
.formLogin() //表单登陆
.loginPage("/login")//登陆判断页面
.loginProcessingUrl("/authentication/form")//自定义form表单登陆提交地址默认是/login
.successHandler(browserAuthenticationSuccessHandler)//自定义登陆成功后返回json信息
.failureHandler(browserAuthenticationFailureHandler)//自定义登陆失败返回json
.and()
.rememberMe() //记住密码
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()) //失效时间
.userDetailsService(userDetailsService)
.and()
.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry).expiredUrl("/login");//用户只能登陆一次
}
}
创建数据库表记录token与过期时间
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
短信验证码
短信验证码接口开发
public class ValidateCode implements Serializable {
//随机数
private String code;
//失效时间
private LocalDateTime exireTime;
public ValidateCode(String code, int exireIn) {
this.code = code;
//当前时间加上过期秒数
this.exireTime = LocalDateTime.now().plusSeconds(exireIn);
}
public ValidateCode(String code, LocalDateTime exireTime) {
this.code = code;
//当前时间加上过期秒数
this.exireTime = exireTime;
}
//判断时间是否过期
public boolean isExpried(){
return LocalDateTime.now().isAfter(exireTime);
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public LocalDateTime getExireTime() {
return exireTime;
}
public void setExireTime(LocalDateTime exireTime) {
this.exireTime = exireTime;
}
}
短信验证码生成
@Component("smsCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator{
@Autowired
private SecurityProperties securityProperties;
/**
* 生成验证码
* @param request
* @return
*/
@Override
public ValidateCode generate(ServletWebRequest request) {
String code= RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLength());
return new ValidateCode(code,securityProperties.getCode().getSms().getExpreIn());
}
}
短信验证码发送
//短信验证码发送
public interface SmsCodeSender {
void send(String mobile,String code);
}
/**
* 默认短信发送器
*/
public class DefaultSmsCodeSender implements SmsCodeSender{
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void send(String mobile, String code) {
logger.info("向手机"+mobile+"发送短信验证码"+code);
}
}
接口生成
@RestController
public class ValidateCodeController {
public static final String SESSION_KEY="SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
@Autowired
private ValidateCodeGenerator imageCodeGenerator;
@Autowired
private ValidateCodeGenerator smsCodeGenerator;
//发送短信
@Autowired
private SmsCodeSender smsCodeSender;
//图形验证码
@RequestMapping(value = "/code/image",method = RequestMethod.GET)
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//生成验证码对象
ImageCode imageCode= (ImageCode) imageCodeGenerator.generate(new ServletWebRequest(request));
//将ImageCode对象保存在session中
sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
//返回到页面
ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
}
//短信验证码
@RequestMapping(value = "/code/sms",method = RequestMethod.GET)
public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletRequestBindingException {
//生成验证码对象
ValidateCode validateCode=smsCodeGenerator.generate(new ServletWebRequest(request));
//将validateCode对象保存在session中
sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,validateCode);
String mobile= ServletRequestUtils.getRequiredStringParameter(request,"mobile");
//发送短信
smsCodeSender.send(mobile,validateCode.getCode());
}
}
短信登录开发
image.png
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private final Object principal;
// ~ Constructors
// ===================================================================================================
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*
*/
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param authorities
*/
public SmsCodeAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
// ~ Methods
// ========================================================================================================
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();
}
}
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String GUOSH_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = GUOSH_FORM_MOBILE_KEY;
private boolean postOnly = true;
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String mobile = this.obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
/**
* 获取手机号
*/
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.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 this.mobileParameter;
}
}
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken=(SmsCodeAuthenticationToken)authentication;
UserDetails user=userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
if(user==null){
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
SmsCodeAuthenticationToken authenticationResult=new SmsCodeAuthenticationToken(user,user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> aClass) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
//短信验证过滤器
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter=new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//成功处理器
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
//失败处理器
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
http.authenticationProvider(smsCodeAuthenticationProvider).addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
package com.guosh.security.core.validate.code.web.filter;
import com.guosh.security.core.properties.SecurityProperties;
import com.guosh.security.core.validate.code.ValidateCode;
import com.guosh.security.core.validate.code.image.ImageCode;
import com.guosh.security.core.validate.code.web.exception.ValidateCodeException;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.util.AntPathMatcher;
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;
import java.util.HashSet;
import java.util.Set;
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {
//登陆失败的处理器
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
private Set<String>urls = new HashSet<>();
private SecurityProperties securityProperties;
private AntPathMatcher antPathMatcher=new AntPathMatcher();
//初始化加载
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
//把要拦截的分割转换数组
if(StringUtils.isNotBlank(securityProperties.getCode().getSms().getUrl())){
String[]configUrls=StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getSms().getUrl(),",");
for (String configUrl:configUrls) {
urls.add(configUrl);
}
}
urls.add("/authentication/mobile");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
//用antPathMatcher工具栏判断路径是否符合
for (String url : urls) {
if (antPathMatcher.match(url, request.getServletPath())) {
action = true;
}
}
//必须是登陆请求并且是post请求
if (action) {
try {
//校验验证码
validate(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
filterChain.doFilter(request,response);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
//从session获取验证码
ValidateCode codeInSession= (ValidateCode) sessionStrategy.getAttribute(request, "SESSION_KEY_FOR_CODE_SMS");
//从请求里获取验证码
String codeInRequest=ServletRequestUtils.getStringParameter(request.getRequest(),"smsCode");
if(StringUtils.isBlank(codeInRequest)){
throw new ValidateCodeException("验证码不能为空");
}
if(codeInSession == null){
throw new ValidateCodeException("验证码不存在");
}
//判断验证码时间是否过期
if(codeInSession.isExpried()){
sessionStrategy.removeAttribute(request, "SESSION_KEY_FOR_CODE_SMS");
throw new ValidateCodeException("验证码已经过期");
}
//比对验证码
if(!StringUtils.equalsIgnoreCase(codeInSession.getCode(),codeInRequest)){
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(request, "SESSION_KEY_FOR_CODE_SMS");
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//自定义配置文件
@Autowired
private SecurityProperties securityProperties;
//处理登陆成功
@Autowired
private BrowserAuthenticationSuccessHandler browserAuthenticationSuccessHandler;
//处理登陆失败
@Autowired
private BrowserAuthenticationFailureHandler browserAuthenticationFailureHandler;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
//数据源
@Autowired
private DataSource dataSource;
//登陆实现
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SessionRegistry sessionRegistry;
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
//密码加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//记住密码
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//启动自动创建persistent_logins表
//tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//验证码拦截器
ValidateCodeFilter validateCodeFilter=new ValidateCodeFilter();
//处理失败异常
validateCodeFilter.setAuthenticationFailureHandler(browserAuthenticationFailureHandler);
//把配置放进去
validateCodeFilter.setSecurityProperties(securityProperties);
//初始化方法
validateCodeFilter.afterPropertiesSet();
//验证码拦截器
SmsCodeFilter smsCodeFilter=new SmsCodeFilter();
//处理失败异常
smsCodeFilter.setAuthenticationFailureHandler(browserAuthenticationFailureHandler);
//把配置放进去
smsCodeFilter.setSecurityProperties(securityProperties);
//初始化方法
smsCodeFilter.afterPropertiesSet();
http
.csrf().disable()//关闭跨站防护
.apply(smsCodeAuthenticationSecurityConfig) //短信登陆配置
.and()
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)//在登陆拦截之前添加验证码拦截器
.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)//在登陆拦截之前添加验证码拦截器
.authorizeRequests()//下面授权配置
.antMatchers("/login",securityProperties.getBrowser().getLoginPage(),"/code/*").permitAll()//login请求除外不需要认证
.anyRequest().authenticated()//所有请求都需要身份认证
.and()
.formLogin() //表单登陆
.loginPage("/login")//登陆判断页面
.loginProcessingUrl("/authentication/form")//自定义form表单登陆提交地址默认是/login
.successHandler(browserAuthenticationSuccessHandler)//自定义登陆成功后返回json信息
.failureHandler(browserAuthenticationFailureHandler)//自定义登陆失败返回json
.and()
.rememberMe() //记住密码
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()) //失效时间
.userDetailsService(userDetailsService)
.and()
.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry).expiredUrl("/login");//用户只能登陆一次
}
}
网友评论