Spring Security是主要解决认证(Authenticate)和授权(Authorization)的框架。
在Spring Boot项目中,添加spring-boot-starter-security依赖项。
注意:以上依赖项是带有自动配置的,一旦添加此依赖,整个项目中所有的访问,默认都是必须先登录才可以访问的,在浏览器输入任何此服务的URL,都会自动跳转到默认的登录页面。
默认的用户名是user,默认的密码是启动项目时自动生成的随机密码,在服务器端的控制台可以看到此密码。
当登录后,会自动跳转到此前尝试访问的页面。
Spring Security默认使用Session机制保存用户的登录状态,所以,重启服务后,登录状态会消失。在不重启的情况下,可以通过 /logout 访问“退出登录”页面,确定后也可以清除登录状态。
在Spring Security中,内置了BCrypt算法的工具类,此工具类可以实现使用BCrypt算法对密码进行加密、验证密码的功能。
BCrypt算法使用了随机盐,所以,多次使用相同的原文进行加密,得到的密文都将是不同的,并且,使用的盐值会作为密文的一部分,也就不会影响验证密码了。
在Spring Security框架中,定义了PasswordEncoder接口,表示“密码编码器”,并且使用BCryptPasswordEncoder实现了此接口。
通常,应该自定义配置类,在配置类中使用@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默认的密码并不是密文,所以对比会失败)。
在默认情况下,Spring Security要求所有的请求都是必须先登录才允许访问的,可以通过Spring Security的配置类对请求放行,即不需要登录即可直接访问。
具体的做法:
SecurityConfiguration继承自WebSecurityConfigurerAdaptervoid 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错误。
使用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:
- <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 方法 提示:在控制器层,处理查询的请求使用@GetMapping配置路径,方法的返回值类型例如:JsonResult>
二:在csmall-passport项目中,实现以下功能,需开发持久层、业务逻辑层、控制器层 \1. 查询角色列表 \2. 插入管理员与角色关联数据(只需要完成持久层)
三:创建新项目,实现与csmall-passport完全相同的功能