使用SpringBoot Security进行登录验证,可以结合具体的业务需求来使用。在
SpringBoot Security前后端分离,登录退出等返回json
一文中,描述了前后端分离的情况下,如何进行登录验证和提示错误信息的。现在针对自定义的登录页面,能够精确地提示错误信息,做一个简单的演示demo。
本文使用的SpringBoot版本是2.1.4.RELEASE,下面直接进入使用阶段。
第一步,在pom.xml中引入架包
org.springframework.boot
spring-boot-starter-security
加上这个架包,重启项目后,整个项目就配置了登录拦截和验证。
第二步,重启项目,会在控制台下看到自动生成的登录密码,默认的用户名是admin
第三步,打开浏览器窗口,对登录页面进行探究
不输入用户名和密码,直接点击登录时,会有提示信息,输入框的颜色还会变红。查看源码,可以发现,架包默认的登录页面提交方式为表单提交,method为post,并且默认是开启csrf的,在表单里自动生成了一个隐藏域,防止跨域提交,确保请求的安全性。
输入错误的用户名或密码,可以看到页面进行了跳转,跳转后的页面又回到了登录页,只是url地址后面多了一个参数,页面提示错误信息。
从页面源码,我们可以获得以下几个方面的信息:
- 自带的登录页面使用post方式提交;
- 用户名密码错误时,页面会进行重定向,重定向到登录页面,并展示错误信息。
如果页面是我们自己自定义的,如果要使用默认的过滤器获取登录信息,则必须使用post方式进行提交,如果使用ajax json的方式进行提交,则获取不到参数。
接下来自定义一个登录页面,为了快速构建登录页面,这里使用了thymeleaf模板。
第一步,在配置文件中,引入thymeleaf架包
org.springframework.boot
spring-boot-starter-thymeleaf
第二步,在resouce的templates下建立登录页面login.html
SpringBoot模版渲染
第三步,添加security的配置文件,为了debug登录情况,对UserDetails、MyPasswordEncoder、UserDetailsService三个接口进行了实现,并在配置文件中进行了配置
package com.example.demo.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.web.filter.CharacterEncodingFilter;
/**
* SpringSecurity的配置
* @author 程就人生
* @date 2019年5月26日
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService myCustomUserService;
@Autowired
private MyPasswordEncoder myPasswordEncoder;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
http
//使用form表单post方式进行登录
.formLogin()
//登录页面为自定义的登录页面
.loginPage("/login")
//设置登录成功跳转页面,error=true控制页面错误信息的展示
.successForwardUrl("/index").failureUrl("/login?error=true")
.permitAll()
.and()
//允许不登陆就可以访问的方法,多个用逗号分隔
.authorizeRequests().antMatchers("/test").permitAll()
//其他的需要授权后访问
.anyRequest().authenticated();
//session管理,失效后跳转
http.sessionManagement().invalidSessionUrl("/login");
//单用户登录,如果有一个登录了,同一个用户在其他地方登录将前一个剔除下线
//http.sessionManagement().maximumSessions(1).expiredSessionStrategy(expiredSessionStrategy());
//单用户登录,如果有一个登录了,同一个用户在其他地方不能登录
http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
//退出时情况cookies
http.logout().deleteCookies("JESSIONID");
//解决中文乱码问题
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8"); filter.setForceEncoding(true);
//
http.addFilterBefore(filter,CsrfFilter.class);
}
// @Bean
// public SessionInformationExpiredStrategy expiredSessionStrategy() {
// return new ExpiredSessionStrategy();
// }
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider bean = new DaoAuthenticationProvider();
//返回错误信息提示,而不是Bad Credential
bean.setHideUserNotFoundExceptions(true);
//覆盖UserDetailsService类
bean.setUserDetailsService(myCustomUserService);
//覆盖默认的密码验证类
bean.setPasswordEncoder(myPasswordEncoder);
return bean;
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(this.daoAuthenticationProvider());
}
}
在这个配置中,对登录页面进行了设置,设置使用自定义的登录页面,在Controller需要添加对应的页面渲染。
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
/**
*
* @author 程就人生
* @date 2019年6月8日
*/
@RestController
public class IndexController {
@RequestMapping("/index")
public ModelAndView index(){
return new ModelAndView("/index");
}
@RequestMapping("/test")
public Object test(){
return "test";
}
/**
* 自定义登录页面
* @param error 错误信息显示标识
* @return
*
*/
@GetMapping("/login")
public ModelAndView login(String error){
ModelAndView modelAndView = new ModelAndView("/login");
modelAndView.addObject("error", error);
return modelAndView;
}
}
package com.example.demo.security;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* 登录专用类,用户登陆时,通过这里查询数据库
* 自定义类,实现了UserDetailsService接口,用户登录时调用的第一类
* @author 程就人生
* @date 2019年5月26日
*/
@Component
public class MyCustomUserService implements UserDetailsService {
/**
* 登陆验证时,通过username获取用户的所有权限信息
* 并返回UserDetails放到spring的全局缓存SecurityContextHolder中,以供授权器使用
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//在这里可以自己调用数据库,对username进行查询,看看在数据库中是否存在
MyUserDetails myUserDetail = new MyUserDetails();
if(StringUtils.isEmpty(username)){
throw new RuntimeException("用户名不能为空!");
}
if(!username.equals("admin")){
throw new RuntimeException("用户名不存在!");
}
myUserDetail.setUsername(username);
myUserDetail.setPassword("123456");
return myUserDetail;
}
}
package com.example.demo.security;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 自定义的密码加密方法,实现了PasswordEncoder接口
* @author 程就人生
* @date 2019年5月26日
*/
@Component
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
//加密方法可以根据自己的需要修改
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return encode(charSequence).equals(s);
}
}
package com.example.demo.security;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
/**
* 实现了UserDetails接口,只留必需的属性,也可添加自己需要的属性
* @author 程就人生
* @date 2019年5月26日
*/
public class MyUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
//登录用户名
private String username;
//登录密码
private String password;
private Collection extends GrantedAuthority> authorities;
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setAuthorities(Collection extends GrantedAuthority> authorities) {
this.authorities = authorities;
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
第四步,重新启动项目,看一下登录页面的效果
一个很丑的登录页面,这不是重点。重点是,登录名和密码正确时,页面可以正确的跳转,输入错误时,可以在登录页面进行信息提示。
在MyCustomUserService类中,我们设置了用户名为admin,密码为123456;输入其他的用户名称时,提示用户不存在;不输入用户名称,提示用户不能为空;密码不是123456时,提示密码错误;输入admin,123456时,页面前往index页面,下面进行验证。
第一步,测试不输入用户名,测试结果ok
第二步,测试输入错误的用户名,测试结果ok
第三步,测试输入正确的用户名和错误的密码,测试结果ok
第四步,测试输入正确的用户名和密码,测试结果ok
第五步,在index.html页面中加个退出按钮,测试一下退出操作,这里为了简单,写了一个表单
SpringBoot模版渲染
index