整合之前默认将除springsecurity之外的配置工作都完成了,数据层框架使用的是mybatis。本篇文章步骤是按照开发顺序编写,也可以从尾端开始倒序观看,注意看代码注释。
本次博客参考:Spring Boot2 + Spring Security5 自定义登录验证(3) 感谢!
导入此依赖时,springsecurity就已经整合到工程中了,已经实现了权限控制(只不过都是springboot默认的配置)
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
首先让实体继承UserDetails类,实现继承方法(UserDetails是Spring Security提供的一个保存用户账号信息的接口)
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Date;
@TableName("tb_user")
public class User implements UserDetails {
@TableId(value = "username")
private String username;//用户名
private String password;//密码,加密存储
private String phone;//注册手机号
private String isMobileCheck;//手机是否验证 (0否 1是)
private Date created;//创建时间
private String name;//真实姓名
private String status;//使用状态(1正常 0非正常)
private String headPic;//头像地址
private String qq;//QQ号码
private String sex;//性别,男,女
private Date birthday;//出生年月日
private Date lastLoginTime;//最后登录时间
@TableField(exist = false) //@TableField为mybatis非字段的属性注解,作用为在一些特殊情况下,也需要在实体中加一些非字段的属性,添加之后使用,但却没有必要加入表中
private Collection<? extends GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public String getUsername() {
return username;
}
/**
* 用户账号是否过期
* true: 未过期
* false: 已过期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 是否锁定
* true: 未锁定
* false: 锁定
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 用户账号凭证(密码)是否过期
* 简单的说就是可能会因为修改了密码导致凭证过期这样的场景
* true: 过期
* false: 无效
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 用户账号是否被启用
* true: 启用
* false: 未启用
* @return
*/
@Override
public boolean isEnabled() {
return true;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getIsMobileCheck() {
return isMobileCheck;
}
public void setIsMobileCheck(String isMobileCheck) {
this.isMobileCheck = isMobileCheck;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getHeadPic() {
return headPic;
}
public void setHeadPic(String headPic) {
this.headPic = headPic;
}
public String getQq() {
return qq;
}
public void setQq(String qq) {
this.qq = qq;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public Date getLastLoginTime() {
return lastLoginTime;
}
public void setLastLoginTime(Date lastLoginTime) {
this.lastLoginTime = lastLoginTime;
}
}
目的是获取与输入用户名相匹配的用户信息,为接下来的认证提供信息。
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.suxianpeng.facefuse.pojo.User;
import com.suxianpeng.facefuse.service.interfaces.UserService;
import org.springframework.beans.factory.annotation.Autowired;
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.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询数据库
System.out.println("username:"+username);
Map map = new HashMap<>();
map.put("username ", username);
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.allEq(map);
User user = userService.findByName(username);
//判断
if (user == null) {//数据库没有用户名,认证失败
throw new UsernameNotFoundException("用户名不存在!");
}
return user;
}
}
UserServiceImpl类中,进行用户查询的方法:
//该方法不仅匹配了用户名进行登录也匹配了使用手机号码进行登录,使用手机号登录要保证数据库中手机号为非加密
@Override
public User findByName(String username) {
User user=new User();
Map mapu = new HashMap<>();
mapu.put("username ", username);
QueryWrapper<User> queryWrapperu = new QueryWrapper<>();
queryWrapperu.allEq(mapu);
user = userMapper.selectOne(queryWrapperu);
if (user!=null){
return user;
}else {
Map mapp = new HashMap<>();
mapp.put("phone", username);
QueryWrapper<User> queryWrapperp = new QueryWrapper<>();
queryWrapperp.allEq(mapp);
user = userMapper.selectOne(queryWrapperp);
if (user!=null) {
return user;
}else{
return null;
}
}
}
继承AuthenticationProvider 认证授权接口,判断用户是否通过认证(可以实现多个认证授权接口来进行不同规则的认证)。
import com.suxianpeng.facefuse.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class LoginValidateAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//获取输入用户名
String username = authentication.getName();
//获取输入明文密码
String rawPassword =(String) authentication.getCredentials();
//根据用户名查询匹配用户
User user = (User)userDetailsService.loadUserByUsername(username);
//判断用户账户状态
if (!user.isEnabled()) {
throw new DisabledException("该账户已被禁用,请联系管理员");
} else if (!user.isAccountNonLocked()) {
throw new LockedException("该账号已被锁定");
} else if (!user.isAccountNonExpired()) {
throw new AccountExpiredException("该账号已过期,请联系管理员");
} else if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException("该账户的登录凭证已过期,请重新登录");
}
//验证密码
if (!passwordEncoder.matches(rawPassword, user.getPassword())) {
throw new BadCredentialsException("输入密码错误!");
}
System.out.println("认证成功!");
return new UsernamePasswordAuthenticationToken(user, rawPassword, user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
//确保authentication能转成该类
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
可以进行认证成功后的一系列处理,如返回给前端结果信息、页面跳转等。
import com.alibaba.fastjson.JSONObject;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Service;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Service
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
String name = SecurityContextHolder.getContext().getAuthentication().getName();
System.out.println("name"+name);
System.out.println("这里是登录成功处理器");
//登录成功返回
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("code", "400");
paramMap.put("message", "登录成功!");
//设置返回请求头
response.setContentType("application/json;charset=utf-8");
//写出流
PrintWriter out = response.getWriter();
out.write(JSONObject.toJSONString(paramMap));
out.flush();
out.close();
}
}
进行认证失败后一系列操作处理
import com.alibaba.fastjson.JSONObject;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Service;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Service
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//登录失败信息返回
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("code", "500");
paramMap.put("message", exception.getMessage());
//设置返回请求头
response.setContentType("application/json;charset=utf-8");
//写出流
PrintWriter out = response.getWriter();
out.write(JSONObject.toJSONString(paramMap));
out.flush();
out.close();
}
}
执行提交认证的用户信息,与接收认证后的返回结果
<el-form :model="loginfo" status-icon ref="loginfo" label-width="100px" class="demo-ruleForm">
<el-form-item label="用户名" prop="username">
<el-input v-model="loginfo.username" placeholder="请输入登录用户名或手机号">el-input>
el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="loginfo.password" placeholder="请输入登录密码">el-input>
el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('loginfo')">登录el-button>
<el-button @click="register()">注册el-button>
el-form-item>
el-form>
<script>
var vm = new Vue( {
el: '#app',
data() {
return {
loginfo: {
password: '',
username: '',
},
}
},
methods: {
submitForm(formName) {
$.ajax({
type: "POST",
url: "/login",
data: this.loginfo,
dataType: "JSON",
success: function (data) {
console.log(data);//打印结果
if (data.code==400){
window.location.href = "/toSystem";//登录成功进行页面跳转
}else {
alert(""+data.message);//提示错误信息
}
}
});
},
}
})
script>
@RequestMapping("/login")
public ModelAndView login(){
ModelAndView modelAndView=new ModelAndView();
modelAndView.setViewName("login");
return modelAndView;
}
@GetMapping("/toSystem")//跳转首页
public ModelAndView toSystem(){
ModelAndView modelAndView=new ModelAndView();
modelAndView.setViewName("system");
return modelAndView;
}
继承自WebSecurityConfigurerAdapter,作用为开启自定义配置,登录成功处理器、失败处理器等,定义规则什么资源拦截,什么可以直接访问等,即上面进行的操作,都要在这个类配置开启才能生效。
注:建议登录页面的路径就使用默认的路径/login退出使用默认/logout,自己在controller中配置跳转的页面为自定义的就可以了,还有注意/login应该为@RequestMapping,因为其要包含支持get与post操作,get操作为请求页面,post操作为进行登录验证。
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.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//自定义认证
@Autowired
private LoginValidateAuthenticationProvider loginValidateAuthenticationProvider;
//登录成功handler
@Autowired
private LoginSuccessHandler loginSuccessHandler;
//登录失败handler
@Autowired
private LoginFailureHandler loginFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置退出操作退出
http.logout()
.logoutSuccessUrl("/login").permitAll();
//配置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/login");
//登录成功之后,跳转路径
http.headers().frameOptions().disable();
http.authorizeRequests()
.antMatchers("/", "/toRegister","/image/*","/user/getUser","/user/addUser","/user/sendSms","/js/*","/css/*").permitAll() //设置哪些路径可以直接访问,不需要认证
//.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login")//登录页面
.loginProcessingUrl("/login")//提交表单时处理登录判断操作
.defaultSuccessUrl("/toSystem")
.successHandler(loginSuccessHandler)//成功登录处理器
.failureHandler(loginFailureHandler)//失败登录处理器
.permitAll();;//其余任何请求都需要通过验证
//关闭csrf跨域攻击防御
http.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {//配置认证方式为自定义认证
auth.authenticationProvider(loginValidateAuthenticationProvider);
}
@Bean
public PasswordEncoder passwordEncoder() {//配置密码编码加密算法为Bcrypt
return new BCryptPasswordEncoder();
}
}
自己的理解:
模拟流程,首先用户登录到需要登录才能访问的页面,此时被重定向到拦截配置的登录页,用户输入用户名、密码向路径 /login POST方式发登录请求,
由于我们在配置中定义了认证方式为auth.authenticationProvider(loginValidateAuthenticationProvider);
,
在经过登录请求拦截与身份验证入口之后,就到我们自定义的身份认证类loginValidateAuthenticationProvider,在该类中通过实现的authenticate方法我们可以得到用户输入的用户名与密文密码,再使用实现的userDetailsService通过用户名到数据库中获取是否存在匹配的用户,用户合规在进行密码的校验,
全部通过之后返回一个Token(UsernamePasswordAuthenticationToken),包含了查询到的匹配用户、用户输入的明文密码、用户的角色权限,开放被限制的资源,并调用认证成功处理器,进行登录成功后的一系列处理,反之登录失败则到失败处理器进行一系列的处理。
注:登录成功后可以使用SecurityContextHolder.getContext().getAuthentication().getName();
来获取登录用户名,在配置成功中映射路径 /login 及其在WebSecurityConfigurerAdapter 的实现类配置上很重要
进行不同权限用户访问不同页面配置:与本篇基本相同主要区别在请求路径处理,与WebSecurityConfigurerAdapter 实现类配置上。推荐看此链接视频理解【狂神说Java】SpringBoot整合SpringSecurity