前言

本文将介绍自定义逻辑实现数据库查询并登录。

使用Spring Security自定义逻辑需要我们重写Spring Security框架中的UserDetailsService接口方法

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

可以看到,UserDetailsService中仅定义了loadUserByUsername方法,用于返回一个UserDetails对象

事实上,UserDetails对象包含了一系列在验证时,需要的信息,例如用户名、密码、权限等等。

Spring Security会根据这些信息判断该用户是否认证成功并授权。

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}
数据库联动实现用户登录认证

既然要查数据库,那么我们先搞一下环境吧!

创建数据库temp,并新建user表

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `username` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
  `password` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键自增',
  `enable` tinyint(2) NOT NULL COMMENT '该用户是否可登录',
  `roles` text CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户角色',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('sunnyboy', '123456', 1, 1, 'admin');

SpringSecurity 自动请求post的 302 springsecurity自定义登录接口_mysql


这里使用Mybatis作为ORM框架,完整的pom如下图所示,主要引入了Spring Boot依赖和Spring Security、Mybatis和一些例如Lombok的工具依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>SpringSecurityDemo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.37</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
    </dependencies>

</project>

按照Mybatis的开发模式,应该编写UserDao,需要注意的是,这里指定@Mapper注解用于声明这是个Mapper,就不需要在启动类上加注解@MapperScan了,当然,反过来也是一样的,两者选其一即可。

@Mapper
public interface UserDao {

    User selectByUsername(@Param("username") String username);
}

对应查询Mapper文件如下,基本语法这里不再赘述。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="ssd.dao.UserDao">
  <resultMap id="BaseResultMap" type="ssd.pojo.User">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="username" jdbcType="VARCHAR" property="username" />
    <result column="password" jdbcType="VARCHAR" property="password" />
    <result column="enable" jdbcType="TINYINT" property="enable" />
    <result column="roles" jdbcType="VARCHAR" property="roles" />
  </resultMap>
  <sql id="Base_Column_List">
    id, username, `password`, `enable`, roles
  </sql>

  <select id="selectByUsername" parameterType="java.lang.String" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from user
    where username = #{username,jdbcType=VARCHAR}
  </select>
</mapper>

新建User对象,需要注意的是,这里User需要实现 UserDetails接口.这是因为我们重写Service实际上是在改逻辑的中间一段,最后还是要返回给Spring Security UserDetails 对象的。

在继承UserDetails后,需要重写isAccountNonExpired、 isAccountNonLocked、isCredentialsNonExpired 这三个方法,这三个方法代表的是账户的可用状态,这里我们暂时都给按账户是可用的,故都设置为返回true。

/**
 * user
 * @author
 */
@Data
public class User implements Serializable, UserDetails {
    /**
     * 主键自增
     */
    private Integer id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 该用户是否可登录
     */
    private Boolean enable;

    /**
     * 用户角色
     */
    private String roles;

    private static final long serialVersionUID = 1L;

    /**
     * 允许的权限
     */
    private List<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    public void setAuthorities(List<GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.enable;
    }
}

重写UserService接口,并且需要在config中指定userService为我们重写的这个。

并且考虑到一般不会使用spring security自带的登陆界面,这里自定义自己的登陆界面

实现自定义登陆界面很简单,只需要在继承WebSecurityConfigurerAdapter的类中声明以下html静态界面的位置即可。

/**
 * @author 阳光大男孩!!!
 */
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private final UserDetailsService userDetailsService;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	// 指定我们自己实现的userDetailService
        auth.userDetailsService(userDetailsService);
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义自己编写的登录页面
        http.formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                    //登录页面设置
                    .loginPage("/Login.html")
                    //登录访问路径
                    .loginProcessingUrl("/user/login")
                    // 登录成功跳转目标url
                    .defaultSuccessUrl("/hello").permitAll()
                	// 设置不需要认证的路径
                		.and().authorizeRequests().antMatchers("/","/index","/user/login").permitAll()
                	// 设置所有请求都需要认证		
                      .anyRequest().authenticated()
                      //设置csrf保护暂时关闭
                .and().csrf().disable();

    }
    @Bean
    PasswordEncoder password()
    {
        return new BCryptPasswordEncoder();
    }
}
/**
 * @author 阳光大男孩!!!
 */
@Service("userDetailsService")
@RequiredArgsConstructor
@Slf4j
public class SecurityUserDetailsServiceImpl implements UserDetailsService {

    private  final UserDao userDao;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println(username);
        User user = userDao.selectByUsername(username);
        if(user==null)
        {
            throw new UsernameNotFoundException("用户不存在");
        }
        List<GrantedAuthority> auths =
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
        user.setAuthorities(auths);
        System.out.println(user);
        user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
        return  user;
    }
}

这里简单自定义了一个登陆界面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
<div id="login">
    <h1>Login</h1>
    <form method="post" action="/user/login">
        <input type="text" required="required" placeholder="用户名" name="username"></input>
        <input type="password" required="required" placeholder="密码" name="password"></input>
        <button class="but" type="submit" >登录</button>
    </form>
</div>
</body>
</html>

考虑登录成功后应当跳转到某个界面,为简单起见,需要设置登陆成功的界面

/**
 * @author 阳光大男孩!!!
 */
@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "Hello Spring Security";
    }
}

指定配置文件内容

server.port=8080
#spring.security.user.name=wyh
#spring.security.user.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.application.name=spring-security-demo
# 配置数据库路径信息
spring.datasource.url=jdbc:mysql://localhost:3306/temp
spring.datasource.username=root
spring.datasource.password=123456
# 指定mapper文件路径
mybatis.mapper-locations=classpath:mapper/*.xml
测试

按照之前的设定,我们指定/hello为受保护的资源路径。

SpringSecurity 自动请求post的 302 springsecurity自定义登录接口_java_02


故需要认证授权,会跳转到登陆界面

SpringSecurity 自动请求post的 302 springsecurity自定义登录接口_spring_03


登录成功后自动跳转到/hello,也就是资源路径

SpringSecurity 自动请求post的 302 springsecurity自定义登录接口_java_04