JSD-2204-关于Spring Security和BCrypt-Day11

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,至少包含:idusernamepasswordenable
    • 还应该查询出此用户名对应的管理员的权限,但此部分暂不实现
  • 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完全相同的功能

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值