本节内容:使用springboot自动security模块实现用户登录验证功能;
登录过程如下图:
AuthenticationManager内容实现用户账号密码验证,还可以对用户状态(启用/禁用),逻辑删除,账号是否被锁定等判断。密码加密方式内置了好几种,我使用的是BCryptPasswordEncoder。那么我们在用户注册时密码要使用 new BCryptPasswordEncoder().encode(pwd)进行加密。
代码实现过程:
1、引入相关依赖;
2、创建UserDetails实现类LoginUser;
3、创建UserDetailsService实现类UserDetailsServiceImpl;
4、SecurityConfiguration配置,将UserDetailsServiceImpl注入相关对象,并配置加密算法;
5、实现账号密码验证;
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
<dependency>
<groupId>eu.bitwalkergroupId>
<artifactId>UserAgentUtilsartifactId>
<version>1.21version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.12.0version>
dependency>
<dependency>
<groupId>com.alibaba.fastjson2groupId>
<artifactId>fastjson2artifactId>
<version>2.0.34version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.9.2version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.9.2version>
dependency>
dependencies>
是数据库用户到Spring用户的转换,提供给Spring内部获取用户账号、状态等的。
package com.luo.chengrui.labs.lab04.model;
import com.alibaba.fastjson2.annotation.JSONField;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* 登录用户身份权限
*
* @author ruoyi
*/
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private String userId;
/**
* 用户唯一标识
*/
private String token;
/**
* 登录时间
*/
private Long loginTime;
/**
* 过期时间
*/
private Long expireTime;
/**
* 登录IP地址
*/
private String ipaddr;
/**
* 登录地点
*/
private String loginLocation;
/**
* 浏览器类型
*/
private String browser;
/**
* 操作系统
*/
private String os;
//数据库用户映射表
private SysUser user;
public LoginUser(String userId, SysUser user) {
this.userId = userId;
this.user = user;
}
public static long getSerialVersionUID() {
return serialVersionUID;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Long getLoginTime() {
return loginTime;
}
public void setLoginTime(Long loginTime) {
this.loginTime = loginTime;
}
public Long getExpireTime() {
return expireTime;
}
public void setExpireTime(Long expireTime) {
this.expireTime = expireTime;
}
public String getIpaddr() {
return ipaddr;
}
public void setIpaddr(String ipaddr) {
this.ipaddr = ipaddr;
}
public String getLoginLocation() {
return loginLocation;
}
public void setLoginLocation(String loginLocation) {
this.loginLocation = loginLocation;
}
public String getBrowser() {
return browser;
}
public void setBrowser(String browser) {
this.browser = browser;
}
public String getOs() {
return os;
}
public void setOs(String os) {
this.os = os;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@JSONField(serialize = false)
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
//账号未过期
@JSONField(serialize = false)
@Override
public boolean isAccountNonExpired() {
return true;
}
//账号未被锁定
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked() {
return true;
}
//密码未过期
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用 ,禁用的用户不能身份验证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isEnabled() {
return true;
}
}
LoginUser类看着字段较多,基本都是和业务相关,比如记录登录用户的IP,地址,登录时间等的。看实际情况,可以优化掉的,仅保留SysUser对象也是完全可以的。
实现根据用户名获取用户信息,提供给spring内部调用的。
package com.luo.chengrui.labs.lab04.service;
import com.luo.chengrui.labs.lab04.model.LoginUser;
import com.luo.chengrui.labs.lab04.model.SysUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 org.springframework.util.StringUtils;
import java.util.Objects;
/**
* 用户验证处理
*
* @author ruoyi
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = userService.selectUserByUserName(username);
if (user == null) {
log.info("登录用户:{} 不存在.", username);
throw new RuntimeException("登录用户:" + username + " 不存在");
} else if (Objects.equals(1, user.getDelFlag())) {
log.info("登录用户:{} 已被删除.", username);
throw new RuntimeException("对不起,您的账号:" + username + " 已被删除");
} else if (Objects.equals(0, user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new RuntimeException ("对不起,您的账号:" + username + " 已停用");
}
return createLoginUser(user);
}
public UserDetails createLoginUser(SysUser user) {
return new LoginUser(user.getUserId(),user);
}
}
1、以上判断用户状态和是否被删除等操作也可以交由spring去做,你只要在LoginUser类中相关方法中返回结果即可。在这里可以做额外的合法性判断。(如这个账号登录次数超限了,并且可以提示相关登录失败信息)
2、实现了根据用户账号查询用户信息方法,最后返回了我们上面定义的LoginUser对象。
package com.luo.chengrui.labs.lab04.service;
import com.luo.chengrui.labs.lab04.model.LoginUser;
import com.luo.chengrui.labs.lab04.utils.UUID;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* @author
* @version 1.0.0
* @description
* @createTime 2024/01/05
*/
@Service
public class LoginService {
@Resource
private AuthenticationManager authenticationManager;
private static final String secret = "abcdefghijklmnopqrstuvwxyz";
public String login(String username, String password) {
// 用户验证
Authentication authentication = null;
try {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
authentication = authenticationManager.authenticate(authenticationToken);
} catch (Exception e) {
//这里应该按不同异常类型分别捕获,更精确的提示用户登录失败的原因。看业务需要
throw new RuntimeException("用户名或密码错误");
} finally {
}
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
return createToken(loginUser);
}
/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser) {
String token = UUID.fastUUID().toString();
loginUser.setToken(token);
Map<String, Object> claims = new HashMap<>();
claims.put("LOGIN_USER_KEY", token);
return createToken(claims);
}
private String createToken(Map<String, Object> claims) {
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
/**
* 刷新令牌有效期,是指刷新缓存中存储的token信息。我们本示例中暂不做缓存。
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser) {
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + 30 * 60 * 1000);
}
}
1、创建鉴权管理器:AuthenticationManager ;
2、设置请求过滤;
3、设置密码加密算法;
4、设置用户获取对象;
package com.luo.chengrui.labs.lab04.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.filter.CorsFilter;
/**
* spring security配置
*
* @author
*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* anyRequest | 匹配所有请求路径
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 过滤请求
.authorizeRequests()
// 所有请求均以放行
.anyRequest().permitAll()
.and()
.headers().frameOptions().disable();
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 设置密码加密算法
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
到此,使用Spring security登录验证的核心代码就写完了。再一个实现UserService类实现从数据库查询用户
package com.luo.chengrui.labs.lab04.service;
import com.luo.chengrui.labs.lab04.model.SysUser;
import com.luo.chengrui.labs.lab04.utils.SecurityUtils;
import com.luo.chengrui.labs.lab04.utils.StringUtils;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author
* @version 1.0.0
* @description
* @createTime 2024/01/31
*/
@Service
public class UserService {
private static final Map<String, SysUser> userMap = new HashMap<>();
static {
userMap.put("admin", new SysUser("1", "admin", SecurityUtils.encryptPassword("admin123"), 1));
}
public List<SysUser> selectUser() {
return userMap.entrySet().stream().map(item -> item.getValue()).collect(Collectors.toList());
}
public SysUser selectUserByUserName(String username) {
SysUser user = userMap.get(username);
if (user == null) {
throw new RuntimeException("用户不存在");
}
return user;
}
public SysUser registerUser(SysUser user) {
if (user == null) {
throw new RuntimeException("用户信息不能为空");
}
if (StringUtils.isEmpty(user.getUsername())) {
throw new RuntimeException("用户名不能为空");
}
if (StringUtils.isEmpty(user.getPassword())) {
throw new RuntimeException("密码不能为空");
}
if (userMap.get(user.getUsername()) != null) {
throw new RuntimeException("用户名已经存在");
}
user.setUserId(UUID.randomUUID().toString());
user.setStatus(1);
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
userMap.put(user.getUserId(), user);
return user;
}
public SysUser selectUser(String username) {
return userMap.get(username);
}
}
简单模拟了根据账号查询用户的接口,实际是事先在userMap中放了一个admin用户而已。
package com.luo.chengrui.labs.lab04.model;
/**
* @author
* @version 1.0.0
* @description
* @createTime 2024/01/31
*/
public class SysUser {
private String userId;
private String username;
private String password;
private Integer delFlag;
private Integer status;
public SysUser() {
}
public SysUser(String userId, String username, String password, Integer status) {
this.userId = userId;
this.username = username;
this.password = password;
this.status = status;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
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 Integer getDelFlag() {
return delFlag;
}
public void setDelFlag(Integer delFlag) {
this.delFlag = delFlag;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}
package com.luo.chengrui.labs.lab04.controller;
import com.luo.chengrui.labs.lab04.model.AjaxResult;
import com.luo.chengrui.labs.lab04.model.LoginBody;
import com.luo.chengrui.labs.lab04.service.LoginService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author
* @version 1.0.0
* @description
* @createTime 2023/07/17
*/
@RestController
@Api(tags = "用户 API 接口")
public class LoginController {
@Autowired
LoginService loginService;
@ApiOperation(value = "用户登录 ", notes = "目前仅仅是作为测试,所以返回用户全列表")
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody) {
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword());
ajax.put("token", token);
return ajax;
}
}
package com.luo.chengrui.labs.lab04.config;
import com.luo.chengrui.labs.lab04.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
/**
* 访问地址:/swagger-ui.html
*
* @author
* @version 1.0.0
* @description
* @createTime 2023/07/17
*/
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
@Autowired
TokenService tokenService;
@Bean
public Docket createRestApi() {
/* 让swagger页面上的每个接口都添加一个Header参数,用来传递token参数*/
ParameterBuilder ticketPar = new ParameterBuilder();
List<Parameter> pars = new ArrayList<Parameter>();
ticketPar.name(tokenService.getHeader()).description("user ticket")//Token 以及Authorization 为自定义的参数,session保存的名字是哪个就可以写成那个
.modelRef(new ModelRef("string")).parameterType("header")
.required(false).build(); //header中的ticket参数非必填,传空也可以
pars.add(ticketPar.build()); //根据每个方法名也知道当前方法在设置什么参数
// 创建 Docket 对象
return new Docket(DocumentationType.SWAGGER_2) // 文档类型,使用 Swagger2
.apiInfo(this.apiInfo()) // 设置 API 信息
// 扫描 Controller 包路径,获得 API 接口
.select()
.apis(RequestHandlerSelectors.basePackage("com.luo.chengrui.labs.lab04.controller"))
.paths(PathSelectors.any())
// 构建出 Docket 对象
.build()
.globalOperationParameters(pars);
}
/**
* 创建 API 信息
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("测试接口文档示例")
.description("我是一段描述")
.version("1.0.0") // 版本号
.contact(new Contact("XX", "http://localhost", "[email protected]")) // 联系人
.build();
}
}
后台方法调用:
1、调用 UserDetailsServiceImpl.LoadUserByUsername方法,获取用户信息;
2、判断用户各种可用状态和密码合法性。
小结:本节主要演示了如何使用Spring去实现登录验证
1、创建UserDetails接口实现类,UserDetails是Spring内部定义的登录用户信息,包含账号、密码、删除状态、禁用状态、锁定状态、密码过期状态;
2、创建UserDetailsService接口实现类,实现loadUserByUsername(String username)方法;
3、用户合法性验证,仅一行代码即可完成用户合法性验证:
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
下一节,咱们可以实现对接口访问的拦截了。