1.Spring Security
1.1关于Spring Security
Spring Security是主要解决认证(Authenticate)和授权(Authorization)的框架。
1.2添加依赖
在Spring Boot项目中,添加spring-boot-starter-security
依赖项。
注意:以上依赖项是带有自动配置的,一旦添加此依赖,整个项目中所有的访问,默认都是必须先登录才可以访问的,在浏览器输入任何此服务的URL,都会自动跳转到默认的登录页面。
默认的用户名是user
,默认的密码是启动项目时自动生成的随机密码,在服务器端的控制台可以看到此密码。
当登录后,会自动跳转到此前尝试访问的页面。
Spring Security默认使用Session机制保存用户的登录状态,所以,重启服务后,登录状态会消失。在不重启的情况下,可以通过 /logout
访问“退出登录”页面,确定后也可以清除登录状态。
1.3关于BCrypt
在Spring Security中,内置了BCrypt算法的工具类,此工具类可以实现使用BCrypt算法对密码进行加密、验证密码的功能。
BCrypt算法使用了随机盐,所以,多次使用相同的原文进行加密,得到的密文都将是不同的,并且,使用的盐值会作为密文的一部分,也就不会影响验证密码了。
在Spring Security框架中,定义了PasswordEncoder
接口,表示“密码编码器”,并且使用BCryptPasswordEncoder
实现了此接口。
1.4在添加管理员时,对密码进行加密
通常,应该自定义配置类,在配置类中使用@Bean
方法,使得Spring框架能创建并管理PasswordEncoder
类型的对象,在后续使用过程中,可以自动装配此对象。
在根包下创建config.SecurityConfiguration
类:
@Configuration
public class SecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
然后,在需要使用此对象的类中,自动装配即可,例如,在AdminServiceImpl
类中添加:
@Autowired
private PasswordEncoder passwordEncoder;
在此类中,就可以使用到以上属性,例如:
String rawPassword = admin.getPassword();
String encodedPassword = passwordEncoder.encode(rawPassword);
admin.setPassword(encodedPassword);
注意:一旦在Spring容器中已经存在PasswordEncoder
对象,Spring Security会自动使用它,所以,会导致默认的随机密码不可用(你提交的随机密码会被加密后再进行对比,而Spring Security默认的密码并不是密文,所以对比会失败)。
1.5对请求放行
在默认情况下,Spring Security要求所有的请求都是必须先登录才允许访问的,可以通过Spring Security的配置类对请求放行,即不需要登录即可直接访问。
具体的做法:
- 使得当前
SecurityConfiguration
继承自WebSecurityConfigurerAdapter
- 重写
void configure(HttpSecurity http)
方法,对特定的请求路径进行访问
package cn.tedu.csmall.passport.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
log.debug("创建密码编码器:BCryptPasswordEncoder");
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 要求请求必须被授权
.antMatchers("/**") // 匹配一些路径
.permitAll() // 允许访问
.anyRequest() // 除以上配置以外的请求
.authenticated(); // 经过认证的
}
}
完成后,重启项目,各页面均可直接访问,不再要求登录!
注意:此时,任何跨域的异步请求不允许提交,否则将出现403
错误。
接下来,还需要在以上配置方法中添加:
http.csrf().disable(); // 禁用防止伪造跨域攻击
如果没有以上配置,则所有的异步跨域访问(无论是否是伪造的攻击)都会被禁止,也就出现了403错误。
1.6使用数据库中的用户名和密码
使用Spring Security时,应该自定义类,实现UserDetailsService
接口,在此接口中,有UserDetails loadUserByUsername(String username)
方法,Spring Security会自动使用登录时输入的用户名来调用此方法,此方法返回的结果中应该包含与用户名匹配的相关信息,例如密码等,接下来,Spring Security会自动使用自动装配的密码编码器对密码进行验证。
所以,应该先将“允许访问的路径”进行调整,然后,自定义类实现以上接口,并重写接口中的方法。
关于“允许访问的路径”,可以将“Knife4j的API文档”的相关路径全部设置为允许直接访问(不需要登录),并且,开启表单验证(使得未授权请求会自动重定向到登录表单),则配置为:
@Override
protected void configure(HttpSecurity http) throws Exception {
// 请求路径白名单
String[] urls = {
"/favicon.ico",
"/doc.html",
"/**/*.js",
"/**/*.css",
"/swagger-resources/**",
"/v2/api-docs"
};
http.csrf().disable(); // 禁用防止伪造跨域攻击
http.authorizeRequests() // 要求请求必须被授权
.antMatchers(urls) // 匹配一些路径
.permitAll() // 允许访问
.anyRequest() // 除以上配置以外的请求
.authenticated(); // 经过认证的
http.formLogin(); // 启用登录表单,未授权的请求均会重定向到登录表单
}
关于自定义的UserDetailsService
接口的实现类:
package cn.tedu.csmall.passport.security;
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.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 假设root是可用的用户名,其它用户名均不可用
if ("root".equals(s)) {
// 返回模拟的root用户信息
UserDetails userDetails = User.builder()
.username("root")
.password("$2a$10$oxvr08D3W0oiesfGPZ8miuPy6kWGst6lz3.qZ29upo8yTjROWh4eC")
.accountExpired(false) // 账号是否已经过期
.accountLocked(false) // 账号是否已经锁定
.credentialsExpired(false) // 认证是否已经过期
.disabled(false) // 是否已经禁用
.authorities("这是临时使用的且无意义的权限值") // 权限,注意,此方法的参数值不可以为null
.build();
return userDetails;
}
throw new UsernameNotFoundException("登录失败,用户名不存在!");
}
}
完成后,重启项目,在启动日志将不会再出现随机的默认密码,并且,可以根据以上方法实现时的用户名+密码实现登录,如果使用错误的用户名或密码,将会提示对应的错误!
接下来,只需要保证以上方法中返回UserDetails
是基于数据库查询来返回结果即可。
则需要:
- 在根包下创建
pojo.vo.AdminLoginInfoVO
,至少包含:id
,username
,password
,enable
- 还应该查询出此用户名对应的管理员的权限,但此部分暂不实现
- 在
AdminMapper
接口中添加抽象方法:AdminLoginInfoVO getLoginInfoByUsername(String username);
- 在
AdminMapper.xml
中配置以上抽象方法映射的SQL语句 - 在
AdminMapperTests
中编写并执行测试 - 在
UserDetailsServiceImpl
中的loadUserByUsername()
方法中通过以上查询来返回结果
关于AdminMapper.xml
:
<!-- AdminLoginInfoVO getLoginInfoByUsername(String username); -->
<select id="getLoginInfoByUsername" resultMap="LoginResultMap">
SELECT
<include refid="LoginQueryFields"/>
FROM
ams_admin
WHERE
username=#{username}
</select>
<sql id="LoginQueryFields">
<if test="true">
id, username, password, enable
</if>
</sql>
<resultMap id="LoginResultMap" type="cn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO">
<id column="id" property="id" />
<result column="username" property="username" />
<result column="password" property="password" />
<result column="enable" property="enable" />
</resultMap>
关于UserDetailsServiceImpl
:
package cn.tedu.csmall.passport.security;
import cn.tedu.csmall.passport.mapper.AdminMapper;
import cn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.stereotype.Service;
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private AdminMapper adminMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("根据用户名【{}】从数据库查询用户信息……", s);
// 调用AdminMapper对象,根据用户名(参数值)查询管理员信息
AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
// 判断是否查询到有效结果
if (loginInfo == null) {
// 根据用户名没有找到任何管理员信息
String message = "登录失败,用户名不存在!";
log.warn(message);
throw new UsernameNotFoundException(message);
}
// 准备返回结果
log.debug("根据用户名【{}】从数据库查询到有效的用户信息:{}", s, loginInfo);
UserDetails userDetails = User.builder()
.username(loginInfo.getUsername())
.password(loginInfo.getPassword())
.accountExpired(false) // 账号是否已经过期
.accountLocked(false) // 账号是否已经锁定
.credentialsExpired(false) // 认证是否已经过期
.disabled(loginInfo.getEnable() == 0) // 是否已经禁用
.authorities("这是临时使用的且无意义的权限值") // 权限,注意,此方法的参数值不可以为null
.build();
log.debug("即将向Spring Security返回UserDetails:{}", userDetails);
return userDetails;
}
}
以上查询管理员的信息时,并没有查询出管理员对应的权限信息,应该补充查询出这部分信息。
作业
一:在csmall-product项目中,实现以下查询功能,需开发持久层、业务逻辑层、控制器层
\1. 查询品牌列表
\2. 查询相册列表
\3. 查询属性模板列表
\4. 根据父级类别id查询类别列表
\5. 根据属性模块id查询属性列表
\6. 根据id查询品牌详情
\7. 根据id查询相册详情
\8. 根据id查询属性详情
\9. 根据id查询属性模板详情
\10. 根据id查询类别详情
目标:通过Knife4j在线API文档可以执行查询请求,返回JSON格式的结果 说明:暂不考虑分页问题 提示:在Service的实现方法中,只需要直接返回Mapper的查询结果即可 提示:你需要在JsonResult类中添加一个public static <T> JsonResult ok(T data)
方法 提示:在控制器层,处理查询的请求使用@GetMapping配置路径,方法的返回值类型例如:JsonResult<List<Brand>>
二:在csmall-passport项目中,实现以下功能,需开发持久层、业务逻辑层、控制器层 \1. 查询角色列表 \2. 插入管理员与角色关联数据(只需要完成持久层)
三:创建新项目,实现与csmall-passport完全相同的功能