首页 > 其他分享 >SpringSecurity基础学习-功能完善

SpringSecurity基础学习-功能完善

时间:2024-07-18 13:54:06浏览次数:14  
标签:功能完善 springframework registerDTO 学习 org import security SpringSecurity public

注册

在数据库中手动维护用户信息是不合理的,同时数据库中存储明文密码也很容易出问题,因此注册接口是一个必须的接口

代码实现

  • Login
package com.learn.security.domain.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import lombok.NonNull;

/**
 * @author PC
 * 登录信息
 */
@Data
@TableName("login")
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class Login {

    public static final String FIELD_ID = "id";
    public static final String FIELD_LOGIN_NAME = "login_name";
    public static final String FIELD_PASSWORD = "password";

    public Login() {
    }

    @TableId(type = IdType.AUTO)
    private Long id;
    @NonNull
    private String loginName;
    @NonNull
    private String password;
}
  • RegisterDTO
package com.learn.security.api.dto;

import lombok.Data;

/**
 * @author PC
 * 注册DTO
 */
@Data
public class RegisterDTO {
    /**
     * 登录名
     */
    private String loginName;
    /**
     * 密码
     */
    private String password;
    /**
     * 验证密码
     */
    private String verifyPassword;

    /**
     * 是否注册成功,1成功0失败
     */
    private Integer successFlag;
}
  • LoginController
package com.learn.security.api.controller;

import com.learn.security.api.dto.RegisterDTO;
import com.learn.security.app.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author PC
 * 登录注册Controller
 */
@RestController("loginController.v1")
@RequestMapping("/v1/login")
public class LoginController {
    private LoginService loginService;

    @Autowired
    public void setLoginService(LoginService loginService) {
        this.loginService = loginService;
    }

    @PostMapping("/register")
    public ResponseEntity<RegisterDTO> register(@RequestBody RegisterDTO registerDTO) {
        return ResponseEntity.ok(loginService.register(registerDTO));
    }
}
  • LoginService
/**
 * 注册
 * @param registerDTO 注册信息
 * @return 注册结果
 */
RegisterDTO register(RegisterDTO registerDTO);
  • LoginServiceImpl
@Override
@Transactional
public RegisterDTO register(RegisterDTO registerDTO) {
    boolean validFlag = !(Objects.isNull(registerDTO) || StringUtils.isEmpty(registerDTO.getLoginName()) || StringUtils.isEmpty(registerDTO.getPassword())
            || StringUtils.isEmpty(registerDTO.getVerifyPassword()));
    //必要信息缺失
    Assert.isTrue(validFlag, "error.necessary.information.missing");
    //两次密码不一致
    Assert.isTrue(StringUtils.equals(registerDTO.getPassword(), registerDTO.getVerifyPassword()), "error.auth_password.wrong");
    //数据库中是否有这个loginName
    Assert.isNull(this.getLoginInfo(registerDTO.getLoginName()), String.format("error.loginName.exist:%s", registerDTO.getLoginName()));
    //密码加密存储
    try {
        registerDTO.setPassword(passwordEncoder.encode(registerDTO.getPassword()));
    } catch (Exception exception) {
        LOGGER.error("encode password failed{}", exception.getMessage());
        registerDTO.setSuccessFlag(0);
        return registerDTO;
    }
    Login newLogin = new Login();
    BeanUtils.copyProperties(registerDTO, newLogin);
    loginMapper.insert(newLogin);
    registerDTO.setSuccessFlag(1);
    return registerDTO;
}
  • SecurityAutoConfiguration(register接口不用授权即可访问)
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            //自定义登录页面
            .formLogin()
            //登陆页面设置
            .loginPage("/login.html")
            //登录url设置
            .loginProcessingUrl("/user/login")
            //登录成功后跳转的路径,如果希望跳回原路径,alwaysUse不填或填false
            .defaultSuccessUrl("/success.html")
            //允许访问
            .permitAll()
            //配置登出
            .and().logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll()
            //未获得授权跳转的页面
            .and().exceptionHandling().accessDeniedPage("/un-auth.html")
            //设置认证权限
            .and().authorizeRequests()
            // /v1/**的没有权限注解的全部放行
            .antMatchers("/v1/**").permitAll()
            .anyRequest().authenticated()
            //关闭csrf防护
            .and().csrf().disable();
    return http.build();
}
  • SecurityConfigProperties
package com.learn.security.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.regex.Pattern;

/**
 * @author PC
 * Security配置属性
 */
@Data
@Configuration
@ConfigurationProperties("cus.security")
public class SecurityConfigProperties {

    /**
     * 角色前缀
     */
    private String rolePrefix = "ROLE_";

    /**
     * 加密算法的正则,默认是BCryptPasswordEncoder
     */
    private Pattern encryptPattern = Pattern.compile("\\A\\$2([ayb])?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
}
  • UserDetailsServiceImpl
package com.learn.security.app.service.impl;

import com.learn.security.app.service.LoginService;
import com.learn.security.config.SecurityConfigProperties;
import com.learn.security.domain.entity.Login;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.util.List;
import java.util.regex.Matcher;

/**
 * @author PC
 * UserDetails实现类
 */
@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    private LoginService loginService;

    private PasswordEncoder passwordEncoder;

    private SecurityConfigProperties securityConfigProperties;

    @Autowired
    public void setLoginService(LoginService loginService) {
        this.loginService = loginService;
    }

    @Autowired
    private void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    @Autowired
    private void setSecurityConfigProperties(SecurityConfigProperties securityConfigProperties) {
        this.securityConfigProperties = securityConfigProperties;
    }

    @Override
    public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {
        // 1. 查询用户
        Login loginInfo = loginService.getLoginInfo(loginName);
        if (loginInfo == null) {
            //这里找不到必须抛异常
            throw new UsernameNotFoundException("User " + loginName + " was not found in db");
        }

        //2. 获取并加密密码
        Assert.isTrue(StringUtils.isNotEmpty(loginInfo.getPassword()), "password deletion");
        String password = loginInfo.getPassword();
        //数据库中可能有一些未加密的密码(之前测试用),这部分在校验的时候需要加密校验,其实可以在注册的时候加一下校验
        Matcher matcher = securityConfigProperties.getEncryptPattern().matcher(loginInfo.getPassword());
        if (!matcher.matches()) {
            password = passwordEncoder.encode(loginInfo.getPassword());
        }

        //3. 赋予user账户一个或多个权限,用逗号分隔,测试可以使用用户名,正式需添加权限字段
        List<GrantedAuthority> grantedAuthorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(loginName);

        return new User(loginName, password, grantedAuthorityList);
    }
}

  • SecurityApplication
@MapperScan("com.learn.security.infra.mapper")
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true)
public class SecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class, args);
    }

}

测试

  • 测试注册接口

  • 测试异常情况

可以看到,接口返回结果并不是我们想要的报错信息,因此需要一个异常处理类

  • 异常处理类
package com.learn.security.infra.handle;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.HandlerMethod;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;


/**
 * @author PC
 */
@RestControllerAdvice
public class ExceptionHandle {
    public final static Logger LOGGER = LoggerFactory.getLogger(ExceptionHandle.class);

    @ExceptionHandler(value = Exception.class)
    public ResponseEntity<Object> handleAssertionError(HttpServletRequest request, HandlerMethod method, Exception ex) {
        if (LOGGER.isWarnEnabled()) {
            LOGGER.warn(String.format("%s/n%s", method.getShortLogMessage(), ex.getMessage()), ex);
        }
        // 构造返回的响应体
        Map<String, Object> responseBody = new HashMap<>();
        responseBody.put("timestamp", LocalDateTime.now());
        responseBody.put("error", "error.error");
        responseBody.put("message", ex.getMessage());
        responseBody.put("path", request.getRequestURI());

        return new ResponseEntity<>(responseBody, HttpStatus.OK);
    }
    
    /**
     * AccessDeniedException交由Security处理
     */
    @ExceptionHandler(value = AccessDeniedException.class)
    public void accessDeniedException(AccessDeniedException accessDeniedException){
        throw accessDeniedException;
    }
}
  • 测试

注册功能完善

在注册时,可能会出现多个用户注册同一个登录名的情况,这种问题可以用分布式锁来解决

创建通用异常类

package com.learn.security.infra.exception;

import lombok.Getter;

/**
 * @author PC
 * 通用异常处理类
 */
@Getter
public class CommonException extends RuntimeException{

    private final transient Object[] parameters;
    private String code;

    public CommonException(String code, Object... parameters) {
        super(code);
        this.parameters = parameters;
        this.code = code;
    }

    public CommonException(String code, Throwable cause, Object... parameters) {
        super(code, cause);
        this.parameters = parameters;
        this.code = code;
    }

    public CommonException(String code, Throwable cause) {
        super(code, cause);
        this.code = code;
        this.parameters = new Object[0];
    }

    public CommonException(Throwable cause, Object... parameters) {
        super(cause);
        this.parameters = parameters;
    }

    public void setCode(String code) {
        this.code = code;
    }
}

注册功能加锁

  • pom中添加redis相关依赖
<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Redisson 依赖 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.13.6</version>
</dependency>
  • application.yml中添加redis相关配置
spring:
  redis:
    host: 192.168.150.128
    port: 6379
    password: 123456
    jedis:
      pool:
        # 资源池中最大连接数
        # 默认8,-1表示无限制;可根据服务并发redis情况及服务端的支持上限调整
        max-active: ${SPRING_REDIS_POOL_MAX_ACTIVE:50}
        # 资源池运行最大空闲的连接数
        # 默认8,-1表示无限制;可根据服务并发redis情况及服务端的支持上限调整,一般建议和max-active保持一致,避免资源伸缩带来的开销
        max-idle: ${SPRING_REDIS_POOL_MAX_IDLE:50}
        # 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)
        # 默认 -1 表示永不超时,设置5秒
        max-wait: ${SPRING_REDIS_POOL_MAX_WAIT:5000}
  • 注册功能加锁
@Override
@Transactional
public RegisterDTO register(RegisterDTO registerDTO) {
    boolean validFlag = !(Objects.isNull(registerDTO) || StringUtils.isEmpty(registerDTO.getLoginName()) || StringUtils.isEmpty(registerDTO.getPassword())
            || StringUtils.isEmpty(registerDTO.getVerifyPassword()));
    //必要信息缺失
    Assert.isTrue(validFlag, "error.necessary.information.missing");
    //两次密码不一致
    Assert.isTrue(StringUtils.equals(registerDTO.getPassword(), registerDTO.getVerifyPassword()), "error.auth_password.wrong");
    //密码加密存储
    try {
        registerDTO.setPassword(passwordEncoder.encode(registerDTO.getPassword()));
    } catch (Exception exception) {
        LOGGER.error("encode password:{} failed{}", registerDTO.getPassword(), exception.getMessage());
        throw new CommonException("error.password.encode");
    }

    RLock rLock = redissonClient.getLock("lock:" + registerDTO.getLoginName());
    try {
        //获取锁等待3秒,10秒过期
        if (rLock.tryLock(3, 10, TimeUnit.SECONDS)) {
            //数据库中是否有这个loginName
            if (Objects.isNull(this.getLoginInfo(registerDTO.getLoginName()))) {
                Login newLogin = new Login();
                BeanUtils.copyProperties(registerDTO, newLogin);
                loginMapper.insert(newLogin);
            } else {
                LOGGER.error(String.format("error.loginName.exist:%s", registerDTO.getLoginName()));
                throw new CommonException("error.loginName.exist");
            }
        }
    } catch (Exception exception) {
        throw new CommonException("error.register:" + exception.getMessage());
    } finally {
        // 释放锁
        rLock.unlock();
    }
    registerDTO.setSuccessFlag(1);
    return registerDTO;
}

测试

暂时将login表的唯一性索引改为普通索引,使用jmeter进行测试

  • 数据库

  • Jmeter

点击运行,查看数据库结果

记住我

当前程序当关闭浏览器后重新打开需要权限校验的页面,会跳回登录页,这个在某些场景是不合理的,因此还需要实现一个记住我的功能

创建自动登陆表

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)

配置类添加自动注册的配置

private UserDetailsService userDetailsService;
@Lazy
@Autowired
public void setUserDetailsService(UserDetailsService userDetailsService) {
    this.userDetailsService = userDetailsService;
}
//com.learn.security.config.SecurityAutoConfiguration#filterChain添加如下配置
//自动登录
.and().rememberMe().tokenRepository(persistentTokenRepository())
//自动登录超时时间
.tokenValiditySeconds(24*60*60)
//设置userDetailService
.userDetailsService(userDetailsService)

页面添加记住我

注意:name的属性值必须为remember-me,不能改为其他值

    <div><label>
        <input type="checkbox" name="remember-me">
    </label>记住密码
    </div>

测试

  • 登出

参考资料

[1]gitee项目仓库地址

标签:功能完善,springframework,registerDTO,学习,org,import,security,SpringSecurity,public
From: https://blog.csdn.net/weixin_43625238/article/details/140518703

相关文章

  • xxe学习笔记
    什么是xxeXXE(XMLExternalEntityInjection)全称为XML外部实体注入,由于程序在解析输入的XML数据时,解析了攻击者伪造的外部实体而产生的。例如PHP中的simplexml_load默认情况下会解析外部实体,有XXE漏洞的标志性函数为simplexml_load_string()。当允许引用外部实体时,通过构造恶......
  • 深度学习框架入门
    #一句话说明白深度学习框架有什么用:利用编程语言来实现复杂的网络架构。不同的开发框架类似不同的语言。常见主流框架介绍 TensorFlow主要用于构建和训练深度学习模型。其强大的可视化工具(如TensorBoard)和对多种硬件的支持,使其在企业级和研究级应用中广泛使用。然而,Ten......
  • Halcon的学习笔记(一)——非线性字符识别
    Halcon非线性模式的字符识别(ocr_cd_print_polar_trans.hdev例程分析)Halcon的学习笔记(一)——非线性字符识别项目上需要对非线性模式的字符进行识别,halcon中包含的例程,我搜了一下,网上对于该例程的解析比较少,因此自己便记录了一下自己的学习例程,也算自己的学习笔记。1.什......
  • 【终极指南】从零开始征服机器学习:初学者的黄金路线图
       踏入机器学习的世界,对许多初学者而言,无疑是一场既兴奋又略带忐忑的冒险。尤其对于那些非数学或计算机专业背景的同学,或是已经在职场打拼的朋友们,寻找一条适合自己的入门路径,显得尤为重要。鉴于此,本文将结合个人经验与导师建议,精心整理一份机器学习学习指南,希望能为渴......
  • 机器学习:详解迁移学习(Transfer learning)
    详解迁移学习深度学习中,最强大的理念之一就是,有的时候神经网络可以从一个任务中习得知识,并将这些知识应用到另一个独立的任务中。所以例如,也许已经训练好一个神经网络,能够识别像猫这样的对象,然后使用那些知识,或者部分习得的知识去帮助您更好地阅读x射线扫描图,这就是所谓的迁移学......
  • Nodify学习 二:添加节点
    Nodify学习一:介绍与使用-可乐_加冰-博客园(cnblogs.com)Nodify学习二:添加节点-可乐_加冰-博客园(cnblogs.com)添加节点(nodes)通过上一篇我们已经创建好了编辑器实例现在我们为编辑器添加一个节点添加model和viewmodel并将它们绑定到视图publicclassNodeViewMod......
  • 【CAN通讯系列3】如何学习CAN通讯?
    对于汽车行业从业者,如何学习CAN通讯,最重要的是取决于你的岗位职责。一方面岗位职责的定义决定你所能获得的资源,另一方面敢岗位职责基本框定了你所使用的CAN通讯内容范围。比如你不是做底层软件开发,那么你很难切身地深入理解CAN通讯的控制流和数据流,因为你没法像底层软件工程师......
  • 小白学习微信小程序开发中的图片和音频处理
    微信小程序开发中,图片和音频的处理是非常常见的功能之一。在本文中,我将详细介绍如何在小程序中进行图片和音频的处理,并提供相关的代码案例。一、图片处理图片的选择和上传在小程序中,用户可以选择图片并上传。首先需要在app.json中注册chooseImage和uploadFile的相关权限。然......
  • 【python学习】第三方库之tensorflow的定义、功能、使用场景、代码示例和解释
    引言tensorFlow是一个开源的软件库,最初由GoogleBrain团队的研究员和工程师开发,用于数值计算和大规模机器学习tensorFlow提供了丰富的API来支持各种计算任务,并且能够轻松地在多种计算设备上进行部署,包括CPU、GPU和TPU文章目录引言一、安装`tensorflow`第三方......
  • Java学习日历(String,StringBuilder,Stringjoiner)
     金额转换packageme.JavaStudy;importjava.util.Scanner;//币值转换publicclassCaptial{publicstaticvoidmain(String[]args){Scannersc=newScanner(System.in);System.out.println("请输入一个数字");intnumber=sc.ne......