【SpringSecurity系列01】初识SpringSecurity

3次阅读

共计 11016 个字符,预计需要花费 28 分钟才能阅读完成。

​ 什么是 SpringSecurity?
​ Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了 Spring IoC,DI(控制反转 Inversion of Control ,DI:Dependency Injection 依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
以上来介绍来自 wiki,比较官方。
​ 用自己的话 简单介绍一下,Spring Security 基于 Servlet 过滤器链的形式,为我们的 web 项目提供认证与授权服务。它来自于 Spring,那么它与 SpringBoot 整合开发有着天然的优势,目前与 SpringSecurity 对应的开源框架还有 shiro。接下来我将通过一个简单的例子带大家来认识 SpringSecurity, 然后通过分析它的源码带大家来认识一下 SpringSecurity 是如何工作,从一个简单例子入门,大家由浅入深的了解学习 SpringSecurity。
通常大家在做一个后台管理的系统的时候,应该采用 session 判断用户是否登录。我记得我在没有接触学习 SpringSecurity 与 shiro 之前。对于用户登录功能实现通常是如下:
public String login(User user, HttpSession session){
//1、根据用户名或者 id 从数据库读取数据库中用户
//2、判断密码是否一致
//3、如果密码一致
session.setAttribute(“user”,user);
//4、否则返回登录页面

}

对于之后那些需要登录之后才能访问的 url,通过 SpringMvc 的拦截器中的 #preHandle 来判断 session 中是否有 user 对象
如果没有 则返回登录页面
如果有,则允许访问这个页面。
但是在 SpringSecurity 中,这一些逻辑已经被封装起来,我们只需要简单的配置一下就能使用。
接下来我通过一个简单例子大家认识一下 SpringSecurity
本文基于 SpringBoot,如果大家对 SpringBoot 不熟悉的话可以看看我之前写的 SpringBoot 入门系列
我使用的是:

SpringBoot 2.1.4.RELEASE
SpringSecurity 5

<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!– lookup parent from repository –>
</parent>
<groupId>com.yukong</groupId>
<artifactId>springboot-springsecurity</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-springsecurity</name>
<description>springboot-springsecurity study</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

配置一下数据库 以及 MyBatis
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/db_test?useUnicode=true&characterEncoding=utf8
password: abc123
mybatis:
mapper-locations: classpath:mapper/*.xml
这里我用的 MySQL8.0 大家注意一下 MySQL8.0 的数据库驱动的类的包改名了
在前面我有讲过 SpringBoot 中如何整合 Mybatis,在这里我就不累述,有需要的话看这篇文章
user.sql
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT ‘ 主键 ’,
`username` varchar(32) NOT NULL COMMENT ‘ 用户名 ’,
`svc_num` varchar(32) DEFAULT NULL COMMENT ‘ 用户号码 ’,
`password` varchar(100) DEFAULT NULL COMMENT ‘ 密码 ’,
`cust_id` bigint(20) DEFAULT NULL COMMENT ‘ 客户 id 1 对 1 ’,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
对应的 UserMapper.java
package com.yukong.mapper;

import com.yukong.entity.User;

/**
*
* @author yukong
* @date 2019-04-11 16:50
*/
public interface UserMapper {

int insertSelective(User record);

User selectByUsername(String username);

}
UserMapper.xml
<?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=”com.yukong.mapper.UserMapper”>
<resultMap id=”BaseResultMap” type=”com.yukong.entity.User”>
<id column=”id” jdbcType=”BIGINT” property=”id” />
<result column=”username” jdbcType=”VARCHAR” property=”username” />
<result column=”svc_num” jdbcType=”VARCHAR” property=”svcNum” />
<result column=”password” jdbcType=”VARCHAR” property=”password” />
<result column=”cust_id” jdbcType=”BIGINT” property=”custId” />
</resultMap>
<sql id=”Base_Column_List”>
id, username, svc_num, `password`, cust_id
</sql>
<select id=”selectByUsername” parameterType=”java.lang.String” resultMap=”BaseResultMap”>
select
<include refid=”Base_Column_List” />
from user
where username = #{username,jdbcType=VARCHAR}
</select>

<insert id=”insertSelective” keyColumn=”id” keyProperty=”id” parameterType=”com.yukong.entity.User” useGeneratedKeys=”true”>
insert into user
<trim prefix=”(” suffix=”)” suffixOverrides=”,”>
<if test=”username != null”>
username,
</if>
<if test=”svcNum != null”>
svc_num,
</if>
<if test=”password != null”>
`password`,
</if>
<if test=”custId != null”>
cust_id,
</if>
</trim>
<trim prefix=”values (” suffix=”)” suffixOverrides=”,”>
<if test=”username != null”>
#{username,jdbcType=VARCHAR},
</if>
<if test=”svcNum != null”>
#{svcNum,jdbcType=VARCHAR},
</if>
<if test=”password != null”>
#{password,jdbcType=VARCHAR},
</if>
<if test=”custId != null”>
#{custId,jdbcType=BIGINT},
</if>
</trim>
</insert>
</mapper>
在这里我们定义了两个方法。
国际惯例 ctrl+shift+ t 创建 mapper 的测试方法,并且插入一条记录
package com.yukong.mapper;

import com.yukong.SpringbootSpringsecurityApplicationTests;
import com.yukong.entity.User;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.junit.Assert.*;

/**
* @author yukong
* @date 2019-04-11 16:53
*/

public class UserMapperTest extends SpringbootSpringsecurityApplicationTests {

@Autowired
private UserMapper userMapper;

@Test
public void insert() {
User user = new User();
user.setUsername(“yukong”);
user.setPassword(“abc123”);
userMapper.insertSelective(user);
}

}
运行测试方法,并且成功插入一条记录。
创建 UserController.java
package com.yukong.controller;

import com.yukong.entity.User;
import com.yukong.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author yukong
* @date 2019-04-11 15:22
*/
@RestController
public class UserController {

@Autowired
private UserMapper userMapper;

@RequestMapping(“/user/{username}”)
public User hello(@PathVariable String username) {
return userMapper.selectByUsername(username);
}

}

这个方法就是根据用户名去数据库查找用户详细信息。
启动。因为我们之前插入过一条 username=yukong 的记录,所以我们查询一下, 访问 127.0.0.1:8080/user/yukong
[图片上传失败 …(image-ea02ac-1554981869345)]
我们可以看到 我们被重定向到了一个登录界面,这也是我们之前引入的 spring-boot-security-starter 起作用了。
大家可能想问了,用户名跟密码是什么,用户名默认是 user,密码在启动的时候已经通过日志打印在控制台了。
现在我们输入用户跟密码并且登录。就可以成功访问我们想要访问的接口。

从这里我们可以知道,我只需要引入了 Spring-Security 的依赖,它就开始生效,并且保护我们的接口了,但是现在有一个问题就是,它的用户名只能是 user 并且密码是通过日志打印在控制台,但是我们希望它能通过数据来访问我们的用户并且判断登录。
其实想实现这个功能也很简单。这里我们需要了解两个接口。

UserDetails
UserDetailsService

所以,我们需要将我们的 User.java 实现这个接口
package com.yukong.entity;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
*
* @author yukong
* @date 2019-04-11 16:50
*/
public class User implements UserDetails {
/**
* 主键
*/
private Long id;

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

/**
* 用户号码
*/
private String svcNum;

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

/**
* 客户 id 1 对 1
*/
private Long custId;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

@Override
public String getUsername() {
return username;
}

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

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

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

@Override
public boolean isEnabled() {
return false;
}

public void setUsername(String username) {
this.username = username;
}

public String getSvcNum() {
return svcNum;
}

public void setSvcNum(String svcNum) {
this.svcNum = svcNum;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 这里我们没有用到权限,所以返回一个默认的 admin 权限
return AuthorityUtils.commaSeparatedStringToAuthorityList(“admin”);
}

@Override
public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public Long getCustId() {
return custId;
}

public void setCustId(Long custId) {
this.custId = custId;
}
}
接下来我们再看看 UserDetailsService

它只有一个方法的声明,就是通过用户名去查找用户信息,从这里我们应该知道了,SpringSecurity 回调 UserDetails#loadUserByUsername 去获取用户,但是它不知道用户信息存在哪里,所以定义成接口,让使用者去实现。在我们这个项目用 我们的用户是存在了数据库中,所以我们需要调用 UserMapper 的方法去访问数据库查询用户信息。这里我们新建一个类叫 MyUserDetailsServiceImpl
package com.yukong.config;

import com.yukong.mapper.UserMapper;
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;

/**
* @author yukong
* @date 2019-04-11 17:35
*/
@Service
public class MyUserDetailServiceImpl implements UserDetailsService {

@Autowired
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userMapper.selectByUsername(username);
}
}

然后新建一个类去把我们的 UserDetailsService 配置进去
这里我们新建一个 SecurityConfig
package com.yukong.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* @author yukong
* @date 2019-04-11 15:08
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userDetailsService;

@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// 配置 UserDetailsService 跟 PasswordEncoder 加密器
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
auth.eraseCredentials(false);
}
}

在这里我们还配置了一个 PasswordEncoder 加密我们的密码,大家都知道密码明文存数据库是很不安全的。
接下里我们插入一条记录,需要注意的是 密码需要加密。
package com.yukong.mapper;

import com.yukong.SpringbootSpringsecurityApplicationTests;
import com.yukong.entity.User;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.junit.Assert.*;

/**
* @author yukong
* @date 2019-04-11 16:53
*/

public class UserMapperTest extends SpringbootSpringsecurityApplicationTests {

@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private UserMapper userMapper;

@Test
public void insert() {
User user = new User();
user.setUsername(“yukong”);
user.setPassword(passwordEncoder.encode(“abc123”));
userMapper.insertSelective(user);
}

}
接下来启动程序,并且登录,这次只需要输入插入到数据中的那条记录的用户名跟密码即可。
在这里一节中,我们了解到如何使用 springsecurity 完成一个登录功能,接下我们将通过分析源码来了解为什么需要这个配置,以及 SpringSecurity 的工作原理是什么。

正文完
 0