本节内容:使用springboot自动security模块实现用户登录验证功能;
登录过程如下图:
AuthenticationManager内容实现用户账号密码验证,还可以对用户状态(启用/禁用),逻辑删除,账号是否被锁定等判断。密码加密方式内置了好几种,我使用的是BCryptPasswordEncoder。那么我们在用户注册时密码要使用 new BCryptPasswordEncoder().encode(pwd)进行加密。
代码实现过程:
1、引入相关依赖;
2、创建UserDetails实现类LoginUser;
3、创建UserDetailsService实现类UserDetailsServiceImpl;
4、SecurityConfiguration配置,将UserDetailsServiceImpl注入相关对象,并配置加密算法;
5、实现账号密码验证;
pom.xml引入依赖
<dependencies><!-- 实现对 Spring MVC 的自动化配置 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- spring security 安全认证 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- Token生成与解析--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><!-- 解析客户端操作系统、浏览器等 --><dependency><groupId>eu.bitwalker</groupId><artifactId>UserAgentUtils</artifactId><version>1.21</version></dependency>
<!-- String工具类 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.12.0</version></dependency><!-- 阿里JSON解析器 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.34</version></dependency><!-- 方便等会写单元测试 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><!-- 引入 Swagger 依赖 --><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><!-- 引入 Swagger UI 依赖,以实现 API 接口的 UI 界面 --><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version></dependency></dependencies>
UserDetails的实现类LoginUser
是数据库用户到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;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@JSONField(serialize = false)@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUsername();}//账号未过期 @JSONField(serialize = false)@Overridepublic boolean isAccountNonExpired() {return true;}//账号未被锁定@JSONField(serialize = false)@Overridepublic boolean isAccountNonLocked() {return true;}//密码未过期@JSONField(serialize = false)@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 是否可用 ,禁用的用户不能身份验证** @return*/@JSONField(serialize = false)@Overridepublic boolean isEnabled() {return true;}
}
LoginUser类看着字段较多,基本都是和业务相关,比如记录登录用户的IP,地址,登录时间等的。看实际情况,可以优化掉的,仅保留SysUser对象也是完全可以的。
UserDetailsService接口实现类 UserDetailsServiceImpl
实现根据用户名获取用户信息,提供给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);@Autowiredprivate UserService userService;@Overridepublic 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对象。
LoginUservice登录实现
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 {@Resourceprivate 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();// 生成tokenreturn 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);}
}
SecurityConfig
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 {/*** 自定义用户认证逻辑*/@Autowiredprivate UserDetailsService userDetailsService;/*** 解决 无法直接注入 AuthenticationManager** @return* @throws Exception*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}/*** anyRequest | 匹配所有请求路径*/@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception {httpSecurity// CSRF禁用,因为不使用session.csrf().disable()// 过滤请求.authorizeRequests()// 所有请求均以放行.anyRequest().permitAll().and().headers().frameOptions().disable();}/*** 强散列哈希加密实现*/@Beanpublic BCryptPasswordEncoder bCryptPasswordEncoder() {return new BCryptPasswordEncoder();}/*** 设置密码加密算法*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());}
}
到此,使用Spring security登录验证的核心代码就写完了。再一个实现UserService类实现从数据库查询用户
UserServiceImpl
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用户而已。
SysUser数据库对象
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;}
}
controller
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 {@AutowiredLoginService 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;}}
swagger配置
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 {@AutowiredTokenService tokenService;@Beanpublic 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", "luodz@gmail.com")) // 联系人.build();}
}
Swaager请求示例:
响应结果:
后台方法调用:
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();
下一节,咱们可以实现对接口访问的拦截了。