SpringBoot security 安全认证(一)——登录验证

本节内容:使用springboot自动security模块实现用户登录验证功能;
登录过程如下图:
SpringBoot security 安全认证(一)——登录验证_第1张图片
AuthenticationManager内容实现用户账号密码验证,还可以对用户状态(启用/禁用),逻辑删除,账号是否被锁定等判断。密码加密方式内置了好几种,我使用的是BCryptPasswordEncoder。那么我们在用户注册时密码要使用 new BCryptPasswordEncoder().encode(pwd)进行加密。

代码实现过程:
1、引入相关依赖;
2、创建UserDetails实现类LoginUser;
3、创建UserDetailsService实现类UserDetailsServiceImpl;
4、SecurityConfiguration配置,将UserDetailsServiceImpl注入相关对象,并配置加密算法;
5、实现账号密码验证;

pom.xml引入依赖

<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>

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;
    }

    @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对象也是完全可以的。

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);

    @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对象。

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 {

    @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);
    }
}


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 {
    /**
     * 自定义用户认证逻辑
     */
    @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类实现从数据库查询用户

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 {

    @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;
    }

}

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 {

    @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();
    }
}

Swaager请求示例:
SpringBoot security 安全认证(一)——登录验证_第2张图片
响应结果:
SpringBoot security 安全认证(一)——登录验证_第3张图片

后台方法调用:
1、调用 UserDetailsServiceImpl.LoadUserByUsername方法,获取用户信息;
SpringBoot security 安全认证(一)——登录验证_第4张图片

2、判断用户各种可用状态和密码合法性。
SpringBoot security 安全认证(一)——登录验证_第5张图片
小结:本节主要演示了如何使用Spring去实现登录验证
1、创建UserDetails接口实现类,UserDetails是Spring内部定义的登录用户信息,包含账号、密码、删除状态、禁用状态、锁定状态、密码过期状态;
2、创建UserDetailsService接口实现类,实现loadUserByUsername(String username)方法;
3、用户合法性验证,仅一行代码即可完成用户合法性验证:

Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();

下一节,咱们可以实现对接口访问的拦截了。

你可能感兴趣的:(SpringBoot,spring,boot,安全,后端)