SpringSecurity基础学习-功能完善

注册

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

代码实现

  • 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项目仓库地址

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Security 是一个开源的安全框架,用于保护基于Spring框架构建的应用程序。MyBatis-Plus 是基于 MyBatis 的增强工具,用于简化 MyBatis 操作和提高开发效率。 将 Spring Security 与 MyBatis-Plus 整合主要包括以下几个步骤: 1. 首先,在 Spring Boot 项目的 pom.xml 文件中添加 Spring Security 和 MyBatis-Plus 的依赖。 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>latest version</version> </dependency> ``` 2. 接下来,在项目的配置文件 application.properties 或 application.yml 中配置数据库连接和 MyBatis-Plus 的相关配置。 3. 创建一个用户实体类和对应的 Mapper 接口,使用 MyBatis-Plus 提供的注解和方法完成数据访问操作。 4. 创建一个自定义的 UserDetailsService 实现类,用于从数据库中获取用户信息,实现 loadUserByUsername 方法。 5. 创建一个自定义的 PasswordEncoder 实现类,用于加密和校验用户密码。 6. 配置 Spring Security,创建一个继承自 WebSecurityConfigurerAdapter 的类,并重写 configure 方法,设置用户认证规则、登录配置和访问控制规则。 7. 在 Spring Boot 启动类上使用 @MapperScan 注解,扫描 Mapper 接口。 通过以上步骤,就可以实现 Spring Security 与 MyBatis-Plus 的整合。在实际应用中,还可以根据具体需求进行一些其他配置和扩展,例如添加角色权限控制、自定义登录页面和处理逻辑等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值