Spring Security 的基本使用

Spring Security 的基本使用

引言:使用 Spring Security 可以保护我们的Spring 程序, 避免某些恶意的请求。以下是有关Spring Security 的一些基本使用。

  1. 添加对应的 Maven 依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.4.1</version>
</dependency>
  1. 添加类对应的 Maven 依赖之后, 运行此程序, 对于所有的URL请求都将会重定向到Spring Security 默认的登录界面。默认的用户名为 “user”, 密码打印在控制台上, 类似于以下这样:
    Default Password在这次的输出中, “51fdbce9-0f29-4445-87ec-0bba83408c8f”就是这次的默认登录密码。

  2. 通过定义一个配置类, 继承 WebSecurityConfigurerAdapter类, 重写父类方法 void configure(AuthenticationManagerBuilder auth)可以实现我们自定义的认证方式。
      方式一: 基于内存的用户认证方式。通过将用户名、用户密码以及对应的用户权限等信息放在程序运行时的对象中,实现用户的认证。值得注意的是, 当设置密码时, Spring 将会检测密码的加密方式, 所以设置的密码格式应当为 {加密方式}加密后的密码的方式。例如, 以下的例子是一个基于内存对象的认证, 用户名为“Jack”, 用户角色为“USER_ROLE”,密码为 “123456”, 加密方式为 bcrypt加密后的密码为$2a$10$o/SQpYXDmLUrGf0IpB/.kOm1y9HSWzNDCxQMXrUTAfDxegNIxPapK现在, 当跳转到登录页面时,输入用户名为 “Jack”, 密码为 “123456” 即可登录。

/*
基于内存中的用户认证对象实现对于用户的验证
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  auth.inMemoryAuthentication()
          .withUser("Jack")
          .password("{bcrypt}$2a$10$o/SQpYXDmLUrGf0IpB/.kOm1y9HSWzNDCxQMXrUTAfDxegNIxPapK")
          .authorities("USER_ROLE");
}

除了“bcrypt”加密方式外, Spring 还提供了 pbkdf2MD5SHA-512等多种加密方式, 具体细节可以查看 org.springframework.security.crypto.factory.PasswordEncoderFactories 以及 org.springframework.security.crypto.password.DelegatingPasswordEncoder
  方式二:基于JDBC 的用户认证。首先设置数据源, 这个在 application.yml配置文件中即可配置, 在使用时注入即可。这样的话, Spring 将会自动帮助我们去查找数据表来验证用户。前提是配置的数据源中具有对应的数据表:usersauthoritiesgroup_authorities等数据表, 因为这是默认的JDBC的SQL进行查找。
  Spring默认的用户信息查找SQL:
Spring默认的用户信息查找SQL:
  如果不想这么做, 那么我们也可以自己配置对应的SQL来完成这一操作。

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
     auth.jdbcAuthentication()
             .dataSource(dataSource)
             .usersByUsernameQuery("SELECT user_name, user_password, is_enabled " +
                     "FROM user_role WHERE user_name=?")
             .authoritiesByUsernameQuery("SELECT user_name, user_role " +
                     "FROM user_role WHERE user_name=?";
 }

不用担心列名是否与Spring 的是否一致, 我们只需要保证对应的列:user_name、user_password、enabled的顺序是正确的即可。因为Spring 对于我们自己编写的SQL语句的用户认证的查找是按照列的索引位置来配置的, 因此保证对应查找的列的位置是对应用户名、用户密码和enabled即可。

  同样的, 如果我们的密码没有满足对应的密码格式{加密方式}加密后的密码,那么我们在登录时将会抛出找不到加密算法的异常, 因此我们必须在最后添加对应的加密方式。这里的加密方式是bcrypt, 因此存储的密码必须是经过bcrypt加密后的密码。

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery("SELECT user_name, user_password, is_enabled " +
                    "FROM user_role WHERE user_name=?")
            .authoritiesByUsernameQuery("SELECT user_name, user_role " +
                    "FROM user_role WHERE user_name=?")
            .passwordEncoder(new BCryptPasswordEncoder());
}

  方式三:自定义用户认证,所对应的实体类应当实现 UserDetails 接口, 同时, 需要创建一个服务类, 实现 UserDetailsService 接口, 从而提供验证用户的功能。
  修改 UserRole 类, 实现 UserDetails 接口:

import lombok.AccessLevel;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import java.util.Collection;

@Data
@Entity
@RequiredArgsConstructor(access = AccessLevel.PUBLIC)
@Table(name = "user_role")
public class UserRole implements UserDetails {
    @Id
    @Column(name = "user_id")
    private Long userId;

    @Basic
    @Column(name = "user_name")
    private String userName;

    @Basic
    @Column(name = "user_password")
    private String userPassword;

    @Basic
    @Column(name = "user_role")
    private String userRole;

    @Basic
    @Column(name = "is_enabled")
    private Boolean enabled;

    // 用户角色信息集合, 这个不是对应表的列, 因此添加 @Transient 注解
    @Transient
    private Collection<SimpleGrantedAuthority> grantedAuthorityCollection;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.grantedAuthorityCollection;
    }

    @Override
    public String getPassword() {
        return this.userPassword;
    }

    @Override
    public String getUsername() {
        return this.userName;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true; // 默认为 true
    }

    @Override
    public boolean isAccountNonLocked() {
        return true; // 默认为 true
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true; // 默认为 true
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }
}

  实现 UserRole 的Repository 接口:

import com.example.springsecurity.Entity.UserRole;
import org.springframework.data.repository.CrudRepository;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
    /**
     * 通过用户名来查找对应的用户对象
     * @param userName : 查找的用户名
     * @return : 查找到的用户角色对象
     */
    UserRole findUserRoleByUserName(String userName);
}

   创建 UserRoleDetailService 服务类, 实现 UserDetailsService 接口。

import com.example.springsecurity.Entity.UserRole;
import com.example.springsecurity.Repository.UserRoleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;

import java.util.ArrayList;
import java.util.Collection;
import java.util.stream.Collectors;

@Service
public class UserRoleDetailService implements UserDetailsService {
    private final UserRoleRepository roleRepository;

    @Autowired
    public UserRoleDetailService(UserRoleRepository roleRepository) {
        this.roleRepository = roleRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        // 定义一个集合, 用于存储用户角色(即权限)的集合。该集合是为了存储用户角色的类型
        Collection<SimpleGrantedAuthority> collection = new ArrayList<>();
        // 遍历所有的用户, 将他们的角色信息添加到集合中
        for (UserRole role: roleRepository.findAll())
            collection.add(new SimpleGrantedAuthority(role.getUserRole()));

        // 使用流过滤掉相同的元素
        collection = collection.stream()
                .distinct()
                .collect(Collectors.toList());

        // 通过用户名查找对应的用户信息
        UserRole userRole = roleRepository.findUserRoleByUserName(username);
        // 将存在的用户角色集合注入到用户信息对象中
        userRole.setGrantedAuthorityCollection(collection);

        return userRole;
    }
}

   现在, 使用 UserDetailsService 服务对象来完成我们自定义的用户验证。同样地, 也需要添加对应的加密方式, 否则Spring 将会解析对应密码, 获取指定的加密方式(这将导致无法找到加密算法,从而抛出异常), 最好的方法还是在方法内定义加密方式, 这里我们设置为 bcrypt加密。
所以最后的 configure() 方法为下面所示:

/*
使用用户自定义的验证方式进行验证
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   auth.userDetailsService(userRoleDetailService)
           .passwordEncoder(new BCryptPasswordEncoder());
}

再次打开 http://127.0.0.1:8080/, 进入登录界面, 输入对应信息, 即可完成验证。

  1. 保护 Web 请求, 有注册才有登录, 完全重定向所有的请求不是一个很好的选择。所以必须设置对应访问路径的请求验证。
      可以通过对某个路径设置访问权限来实现这个目标, 比如, 我们希望将 “/better’ 和 ”/wonderful“ 访问路径设置为只有角色为”ROLE_ADMIN“ 的角色才能访问, 而对于”/home“路径对于所有的用户都可以访问, 那么可以这么做:
    注意: Spring 对于用户角色的定义, 是以 “ROLE_”为前缀, 后面再接具体的角色名。比如:ROLE_ADMIN 则表示这是一个 ADMIN 的角色。
@Override
protected void configure(HttpSecurity http) throws Exception {
     http.authorizeRequests()
             .antMatchers("/better", "/wonderful")
                 .hasRole("ROLE_ADMIN")
             .antMatchers("/home").permitAll()
 }

除了 hasRole()permitAll() 之外, Spring 还提供了许多的路径配置方法:保护路径的配置方法
同时, access() 方法通给定的 SpEL (Spring Expression Language Spring扩展语言),判断 SpEL结果 来进行访问验证。
在这里插入图片描述
我们希望对于对于除了某些指定的URL外, 对于其它URL的访问都需要经过认证后才能访问, 那么我们可以在最后的认证后添加 .anyRequest().authenticated()来实现, 具体代码如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
   http
       	.authorizeRequests()
           .antMatchers("/wonderful") 
               .access("hasRole('ROLE_ADMIN')") 
               /* '/wonderful' 的请求需要用户具有 “ROLE_ADMIN”的角色*/
           .antMatchers("/home", "/login")
               .access("permitAll()") 
               /* 对于 “/home”, "/login" 允许所有的请求访问。
               “/login”在这里被允许任何请求访问是必须的, 因为所有的需要认证的
               请求都会重定向到 “/login”, 如果 “/login” 也是需要被认证的,
                即不是允许所有请求的访问, 那么“/login” 的请求将不断重定向
                到“/login”, 直至浏览器崩溃。*/
                /*
                	‘/login’ 是登录的界面的URL, 它是可以修改的, 
                	因此当 “/login” 修改为其它的URL时, 请务必将它设置为是允许所有请求访问的。
                */
               .anyRequest()
               .authenticated();
}

这里需要注意的地方便是对于 “/login”的访问, 它应当是允许所有的请求访问的, 如果不是的, 那么将会导致浏览器的崩溃。

要处理登录, Spring 为我们提供了默认的对于Post/login URL。因此我们在编写HTML的登录表单时, 需要将处理路径, 即 formaction 属性设置为/login。用户名输入框的 name 属性请设置为 username, 密码输入框的 name 属性请设置为 password, 因为这是Spring 的默认参数名, 如果不想折腾的话, 还是按照它原有的设计是一个更好的选择。

<form th:action="@{/login}" method="post">
    <!-- Email input -->
    <div class="form-outline mb-4">
        <input type="text" id="username" name="username" class="form-control"/>
        <label class="form-label" for="username">user name</label>
    </div>
    <!-- Password input -->
    <div class="form-outline mb-4">
        <input type="password" id="password" name="password" class="form-control"/>
        <label class="form-label" for="password">Password</label>
    </div>
    <div class="col">
        <!-- Simple link -->
        <a th:href="@{/register}">Register</a>
    </div>
    <!-- Submit button -->
    <button type="submit" class="btn btn-primary btn-block">Sign in</button>
</form>

如果想替换默认的登录界面, 可以通过设置 HttpSecurity 的 formLogin().loginPage(String) 方法即可, 方法的参数名为处理登录的页面。通过 defaultSuccessUrl() 可以设置认证成功后跳转到的URL。示例如下:

.and()
	.formLogin()
	    .loginPage("/login") 
	    /* 	
	    	用户认证登录界面的 URL, 
	    	请注意这个 URL 应当是允许任何请求访问的
	    */
	    .loginProcessingUrl("/login")
	    /*
	    	登录信息提交的处理URL, 由于Spring为我们提供了默认的
	    	“/login”处理URL, 因此设置为 “/login”
	    */
	    .defaultSuccessUrl("/better", true)
	    /*
	    	认证成功后的默认跳转界面, 如果不设置第二个参数, 或者设置为false,
	    	那么在认证成功后将会访问之前被重定向的页面。
	    	设置第二个参数为true将强制在认证成功后跳转到指定的页面, 在这里是 “/better”
	    */
	    .failureUrl("/error")
	    /*
	    	认证失败时的默认重定向URL。
	    */

使用 HttpSecuritylogout() 方法完成退出登录状态动作。Spring 为我们提供了默认的退出登录URL “/logout”, 它应当是允许所有的i请求访问的。

.and()
     .logout() // 退出登录
     .logoutSuccessUrl("/home") // 退出登录后的页面 URL
     .permitAll(); // 允许任何请求访问
  1. 了解发送请求的用户。对于已经认证过的用户, Spring 提供了四种方式获取认证的用户信息。
      方式一:注入Principal对象到控制器方法中
 /**
 * 注入Principal对象到控制器方法中, 获取已认证的用户信息
  * @param principal : 注入的 Principal 对象
  * @return : 得到的用户信息
  */
 @GetMapping(path = "/checkUserByPrincipal")
 public @ResponseBody
 String checkUserByPrincipal(Principal principal) {
     UserRole role = roleRepository.findUserRoleByUsername(principal.getName());

     assert role != null; // 用户应当是已经登录的, 所以它应当是已经存在的。

     return role.toString();
 }

  方式二:注入 Authentication 对象到控制器方法中

/**
* 注入 Authentication 对象到控制器方法中, 获取已认证的用户信息
* @param authentication : 注入的 Authentication 对象
* @return : 获取到的用户信息
*/
@GetMapping(path = "/checkUserAuth")
public @ResponseBody
String checkUser(Authentication authentication) {
   UserRole userRole = (UserRole) authentication.getPrincipal();

   return userRole.getUserRole();
}

  方式三:添加 @AuthenticationPrincipal 注解到用户信息对象上, 表明这个用户信息对象是认证的 Principal 对象。

/**
* 添加 @AuthenticationPrincipal 注解,
 * 表明这个 UserRole 对象是一个已经认证的 Principal 对象。
 * @param userRole : 认证过的 principal 对象
 * @return : 认证的 UserRole 对象
 */
@GetMapping(path = "/checkUserByAuthAnnotation")
public @ResponseBody
String checkUserByAuthAnnotation(@AuthenticationPrincipal UserRole userRole) {
    return userRole.toString();
}

  方式四:通过上下文来获取已认证的用户信息。

/**
 * 通过上下文来获取已认证的用户信息
 * @return : 根据上下问获取到的用户信息
 */
@GetMapping(path = "/checkUserByContext")
public @ResponseBody
String checkUserByContext() {
    // 获取上下文的认证信息
    Authentication authentication = SecurityContextHolder
            .getContext().getAuthentication();
    UserRole userRole = (UserRole) authentication.getPrincipal();

    assert userRole != null;

    return userRole.toString();
}

推荐使用后三种方式。

示例地址:https://github.com/LiuXianghai-coder/Spring-Study/tree/master/spring-security

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值