zl程序教程

您现在的位置是:首页 >  后端

当前栏目

springboot集成security(认证)

2023-09-11 14:22:32 时间

1. 依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.1</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

加入 spring-boot-starter-security 依赖之后,不需要任何配置,启动项目,就会出现一个登录界面:

  • 任何访问请求都会被拦截到 /login,并且要求认证后才能访问,请求方式是 Get

  • 默认 usernameuser,每次启动都会在控制台输出一个 128Byte 的 password

  • 可通过配置文件修改默认的 usernamepassword (静态用户,适用于内部网络认证)

    spring:
      # default login url: /login
      security:
        user:
          name: dev
          # default size: 128 Byte
          password: admin
    

2. 自定义登录逻辑


1. 数据库查询

表、实体类:

@Data
public class User {
    private Integer id;
    private String name;
    private String password;
}

配置文件:

# application name
spring:
  # mysql
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spbt?serverTimezone=Asia/Shanghai
    username: root
    password: admin
# web service port
server:
  port: 8088
mybatis-plus:
  type-aliases-package: com.chenjy.security_demo.dto
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

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="com.chenjy.security_demo.mapper.UserMapper">
    <select id="getUserByName" parameterType="string" resultType="user">
        select
               id,
               name,
               password
        from
             user
        <where>
            <if test="name != null and name != ''">
                name = #{name}
            </if>
        </where>
    </select>
</mapper>
@Mapper
public interface UserMapper {
    User getUserByName(String name);
    List<User> getUserByName();
}

启动类加上 @MapperScan 注解


service:

public interface UserService {
    User getUserByName(String name);
    List<User> getUserByName();
}
@Service
public class UserServiceImpl implements UserService {
    @Resource
    private UserMapper userMapper;

    @Override
    public User getUserByName(String name) {
        return userMapper.getUserByName(name);
    }

    @Override
    public List<User> getUserByName() {
        return userMapper.getUserByName();
    }
}

2. security认证

要自定义 security 认证逻辑,就需要定义一个服务对象来实现 UserDetailsService 接口,并重写 loadUserByUsername 方法。


1. loadUserByUsername

loadUserByUsername

  • security 唯一的认证方法
  • 查询用户失败,会抛出 UsernameNotFoundException
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        com.chenjy.security_demo.dto.User user = userService.getUserByName(username);
        if (user == null) {
            throw  new UsernameNotFoundException("用户名错误");
        }
        // 匹配用户密码
        // org.springframework.security.core.userdetails.User
         User res = new User(username, user.getPassword(), AuthorityUtils.createAuthorityList());
        return res;
    }
}
  • org.springframework.security.core.userdetails.User

  • loadUserByUsername 需要返回一个 UserDetails 的实现类,可以直接使用 User,其有两个构造器:

    public User(String username, String password, Collection<? extends GrantedAuthority> authorities)
    public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) 
    
    • username 用户名
    • password 用户密码
    • authorities 权限集合
  • security 内部会自动进行密码匹配

这时候直接启动项目,会报错:

*java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"*

这是因为 security 5.X 需要提供一个PasswordEncorder的实例


2. PasswordEncorder(不加密)

security 内部进行密码匹配的时候,一定要进行加密和解密处理。要求 IOC 容器中,必须要存在一个 PasswordEncorder 对象,提供加密和解密逻辑。

  • 可以直接客户端明文,数据库解密来匹配验证
  • 也可以客户端加密,数据库直接密文来匹配验证

因为我的数据库密码并未进行加密,所以这里,我们就直接返回字符串进行明文比对就行了。

@Component
public class MyPasswordEncoder implements PasswordEncoder {
    /**
     * @Description 收集页面的密码
     * @param rawPassword
     * @return String
    */
    @Override
    public String encode(CharSequence rawPassword) {
        return rawPassword.toString();
    }

    /**
     * @Description 匹配逻辑
     * @param rawPassword 明文,页面收集的密码
     * @param encodedPassword 密文,存储在数据源中的密码
     * @return boolean
    */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return rawPassword.equals(encodedPassword);
    }
}

encode 对页面收集到的密码进行加密,然后将加密好的密文交给 matches,与数据库密码进行比对。


3. MD5加密数据库密码

自定义加密工具类

public class PwdEncode {

    /**
     * @Description 收集
     * @param pwd
     * @return String
    */
    public static String encode(String pwd) {
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (Exception e) {
            System.out.println(e.toString());
            e.printStackTrace();
            return "";
        }
        char[] charArray = pwd.toCharArray();
        byte[] byteArray = new byte[charArray.length];

        for (int i = 0; i < charArray.length; i++) {
            byteArray[i] = (byte) charArray[i];
        }
        byte[] md5Bytes = md5.digest(byteArray);
        StringBuffer hexValue = new StringBuffer();
        for (int i = 0; i < md5Bytes.length; i++) {
            int val = ((int) md5Bytes[i]) & 0xff;
            if (val < 16){
                hexValue.append("0");
            }
            hexValue.append(Integer.toHexString(val));
        }
        return hexValue.toString();
    }

    /**
     * @Description 解密
     * 一次加密之后,需要两次解密
     * @param pwd
     * @return String
    */
    public static String decrypt(String pwd) {
        char[] a = pwd.toCharArray();
        for (int i = 0; i < a.length; i++) {
            a[i] = (char) (a[i] ^ 't');
        }
        String s = new String(a);
        return s;
    }

    public static void main(String[] args) {
        String s = "123456";
        System.out.println("原始:" + s);
        System.out.println("MD5后:" + encode(s));
        System.out.println("加密的:" + decrypt(s));
        System.out.println("解密的:" + decrypt(decrypt(s)));
    }
}

然后修改数据库密码。


4. PasswordEncorder(加密)

@Component
public class MyPasswordEncoder implements PasswordEncoder {
    /**
     * @Description 收集页面的密码
     * @param rawPassword
     * @return String
    */
    @Override
    public String encode(CharSequence rawPassword) {
        return rawPassword.toString();
    }

    /**
     * @Description 匹配逻辑
     * @param rawPassword 明文,页面收集的密码
     * @param encodedPassword 密文,存储在数据源中的密码
     * @return boolean
    */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return PwdEncode.encode(rawPassword.toString()).equals(encodedPassword);
    }
}

注意: MD5解密很简单——https://www.cmd5.com/,所以不推荐使用MD5加密。


5. BCryptPasswordEncoder

鉴于MD5加密的不安全性,所以建议使用 security 自带的加密工具类 —— BCryptPasswordEncoder

  • 同一明文,加密两次,输出不同

        public static void main(String[] args) {
            String s = "123456";
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();;
            System.out.println("原始: " + s);
            System.out.println("加密1: " + encoder.encode(s));
            System.out.println("加密1: " + encoder.encode(s));
        }
    
  • 可使用 matches 验证明文与密文是否相同:

    System.out.println(encoder.matches(s, encoder.encode(s)));
    

BCryptPasswordEncoder使用bcrypt算法对密码进行加密,同时会为密码加上“盐”,开发者不需要自己加“盐”,即使相同的明文字段生成的加密字符串也不同。匹配时,从密文中取出“盐”,用该盐值加密明文和最终密文作对比。


BCryptPasswordEncoder的默认强度为10,开发者可以根据自己服务器的速度进行调整,以确保密码验证的时间约为1秒(官方建议)

BCryptPasswordEncoder encoder_pro = new BCryptPasswordEncoder(15);
System.out.println("加密2: " + encoder_pro.encode(s));

注册 BCryptPasswordEncoder

  • 取消MyPasswordEncoder的 @Component 注解

  • 新建一个配置类,注册 BCryptPasswordEncoder

    @Configuration
    public class SecurityConf {
        @Bean
        public BCryptPasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    

6. 认证流程(图)


3. 自定义登录界面


1. 界面

依赖:

        <!--thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

resources/templates/login.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>登录</title>
</head>
<body>
<div class="main">
  <div class="title">
    <span>密码登录</span>
  </div>

  <div class="title-msg">
    <span>请输入登录账户和密码</span>
  </div>

  <form class="login-form" method="post" novalidate th:action="@{/myLogin}">
    <!--输入框-->
    <div class="input-content">
      <!--autoFocus-->
      <div>
        <input type="text"
               autocomplete="off"
               placeholder="用户名"
               name="name"
               required/>
      </div>

      <div style="margin-top: 16px">
        <input type="password"
               autocomplete="off"
               placeholder="登录密码"
               name="password"
               required
               maxlength="32"/>
      </div>
    </div>

    <!--登入按钮-->
    <div style="text-align: center">
      <button type="submit" class="enter-btn" >登录</button>
    </div>
  </form>

</div>
</body>
<style>
  body{
    background: #426258;
  }
  *{
    padding: 0;
    margin: 0;
  }

  .main {
    padding-left: 25px;
    padding-right: 25px;
    padding-top: 15px;
    width: 350px;
    height: 350px;
    background: #FFFFFF;
    /*以下css用于让登录表单垂直居中在界面,可删除*/
    position: absolute;
    top: 50%;
    left: 50%;
    margin: -175px auto 0 -175px;
  }

  .title {
    width: 100%;
    height: 40px;
    line-height: 40px;
  }

  .title span {
    font-size: 18px;
    color: #353f42;
  }

  .title-msg {
    width: 100%;
    height: 64px;
    line-height: 64px;
  }

  .title:hover{
    cursor: default	;
  }

  .title-msg:hover{
    cursor: default	;
  }

  .title-msg span {
    font-size: 12px;
    color: #707472;
  }

  .input-content {
    width: 100%;
    height: 120px;
  }

  .input-content input {
    width: 330px;
    height: 40px;
    border: 1px solid #dad9d6;
    background: #ffffff;
    padding-left: 10px;
    padding-right: 10px;
  }

  .enter-btn {
    width: 350px;
    height: 40px;
    color: #fff;
    background: #0bc5de;
    line-height: 40px;
    text-align: center;
    border: 0px;
  }

  .enter-btn:hover {
    cursor:pointer;
    background: #1db5c9;
  }
</style>

</html>

注意: security 处理登录请求的方式一定是 POST

resources/templates/main.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>主页</title>
</head>
<body>
<div class="con">
    <h1><span style="color: #67865d">登陆成功</span></h1>
    <br>
    <div class="component">
    </div>
    <br>
</div>
</body>
<style>
    body {
        background-color: #c1d3d0;
    }
    .con {
        text-align: center;
    }
    .component div {
        font-size: 32px;
    }
    .component span {
        color: #FFFFFF;
        font-size: 32px;
    }
</style>

2. security配置类

extends WebSecurityConfigurerAdapter 并重写 configure(HttpSecurity http)


1. 配置默认登录界面

@Configuration
public class SecurityConf extends WebSecurityConfigurerAdapter {
    /**
     * @Description 注册BCryptPasswordEncoder到容器
     * @return BCryptPasswordEncoder
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * @Description 重写父类型中的配置逻辑
     * @param http 基于http协议的security配置对象,包含所有security相关配置逻辑
     * @throws Exception 配置出错会抛出异常
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 配置请求相关内容
        http
                .formLogin()
                .loginPage("/myLogin") // 登录页面地址,默认 /login, 这里的地址要由 controller 接口地址决定
                .usernameParameter("name") // 登录页面用户名字段
                .passwordParameter("password"); // 登录页面用户密码字段

        // 关闭csrf
        http.csrf().disable();
    }
}

然后启动:

  • 可发现,并不会拦截请求
  • 一旦自定义配置,所有的默认认证流程会全部清空

2. 配置拦截与放行

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 配置请求相关内容
        http
                .formLogin()
                .loginPage("/myLogin") // 登录页面地址,默认 /login, 这里的地址要由 controller 接口地址决定
                .usernameParameter("name") // 登录页面用户名字段
                .passwordParameter("password") // 登录页面用户密码字段
                .and()
                .authorizeRequests()
                .antMatchers("/myLogin")
                .permitAll() // /myLogin 不需要认证,可直接访问
                .anyRequest()
                .authenticated(); // 其他请求都需要认证

        // 关闭csrf
        http.csrf().disable();
    }
  • formLogin 配置登录表单
  • loginPage 配置登录界面
  • usernameParameter 登录页面用户字段名
  • passwordParameter 登录页面用户密码字段名
  • and and
  • authorizeRequests 配置拦截和放行的url
  • antMatchers 匹配多个路径,用 , 隔开
  • permitAll 前面匹配的路径全部放行
  • anyRequest 其他路径
  • authenticated 匹配的路径需要认证

注意:

  • 拦截一定要写在放行之后
  • 如果不配置 usernameParameterpasswordParameter,那么请求参数名必须是 usernamepassword,不然 security 无法识别

3. 登录成功后的处理方案

security 默认访问成功后跳转到 “/” ,现在修改配置,使其跳转到 main


方案一: 使用 successForwardUrl,请求转发 —— POST 请求。

    @RequestMapping("main")
    public String main() {
        return "main";
    }
.successForwardUrl("/main") // 登录成功跳转页面

方案二: 使用 successForwardUrl,响应重定向(重新请求) —— GET 请求。

.defaultSuccessUrl("http://127.0.0.1:8088/main") // 响应重定向(GET)

注意1: 因为 successForwardUrl 是重新发起请求,所以需要传入一个绝对地址才能成功定向。

注意2: 最好传入一个参数 alwaysUse (代表一定使用这个重定向地址),防止出现错误。

.defaultSuccessUrl("http://127.0.0.1:8088/main", true) 

方案三: 使用 successHandler,自定义认证成功后的请求处理逻辑(转发、重定向都可自定义)。

successForwardUrldefaultSuccessUrl 内部就是用的 successHandler 的实现类,进行控制成功后交给哪个方法处理。

自定义请求成功处理器: implements AuthenticationSuccessHandler

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SuccessHandler implements AuthenticationSuccessHandler {
    private String url;
    public boolean isRedirect;
    
    /**
     * @Description
     * @param request 请求对象  请求转发——request.getRequestDispatcher(url).forward(request, response);
     * @param response 响应对象 重定向——response.sendRedirect(url);
     * @param authentication 认证成功后的对象,包含用户名和权限信息(防止信息泄露,所以没带密码)
    */
    @Override
    public void onAuthenticationSuccess( HttpServletRequest request,
            HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        if (isRedirect) {
            response.sendRedirect(url);
        } else {
            request.getRequestDispatcher(url).forward(request, response);
        }
    }
}
.successHandler(new SuccessHandler("/main", false)) // 使用自定义请求处理控制逻辑

4. 登陆失败后的处理方案

登陆失败后,默认跳转到 /login?error,现在自定义一个登录失败的处理。


方案一: 使用 failureForwardUrl,登录失败请求转发 —— POST

    @RequestMapping("fail")
    public String fail(Model model) {
        model.addAttribute("fail");
        return "login";
    }
.failureForwardUrl("/fail") // 登录失败转发
<span th:if="${fail} == 'true'" style="color: red">登陆失败</span>

方案二: 使用 failureForwardUrl,响应重定向。

.failureUrl("/fail") // 响应重定向
...
.antMatchers("/myLogin", "/fail")
.permitAll() // /myLogin,/fail 不需要认证,可直接访问

重定向是重新发请求, 所以需要在权限配置中放行 /fail


方案三: 使用 failureForwardUrl,自定义处理器。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class FailureHandler implements AuthenticationFailureHandler {
    private String url;
    private boolean isRedirect;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        if (isRedirect) {
            response.sendRedirect(url);
        } else {
            request.getRequestDispatcher(url).forward(request, response);
        }
    }
}
.failureHandler(new FailureHandler("/fail", true)) //