Spring Security 入门 (二)

我们在篇(一)中已经谈到了默认的登录页面以及默认的登录账号和密码。

在这一篇中我们将自己定义登录页面及账号密码。

我们先从简单的开始吧:设置自定义的账号和密码(并非从数据库读取),虽然意义不大。

上一篇中,我们仅仅重写了 configure(HttpSecurity http) 方法,该方法是用于完成用户授权的。

为了完成自定义的认证,我们需要重写 configure(AuthenticationManagerBuilder auth) 方法。

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // TODO Auto-generated method stub
        auth.inMemoryAuthentication().withUser("Hello").password("{noop}World").roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // TODO Auto-generated method stub
        http.authorizeRequests()
        .antMatchers("/login").permitAll()
        .antMatchers("/user").hasRole("USER")
        .anyRequest().authenticated()
        .and()
        .formLogin().defaultSuccessUrl("/hello");
    }
}

这个就是新的 WebSecurityConfig 类,控制器里面的方法我就不写了,仿照(一)很容易写出来,运行结果你们自己测试吧。

configure(AuthenticationManagerBuilder auth) 方法中,AuthenticationManagerBuilder 的 inMemoryAuthentication() 方法

可以添加用户,并给用户指定权限,它还有其他的方法,我们以后用到再讲。

在 Password 的地方我们需要注意了:

Spring 5.0 之后为了更加安全,修改了密码存储格式,密码存储格式为{id}encodedPassword。

id 是一个标识符,用于查找是哪个 PasswordEncoder,也就是密码加密的格式所对应的 PasswordEncoder。

encodedPassword 是指原始密码经过加密之后的密码。id 必须在密码的开始,id前后必须加 {}。

如果 id 找不到,id 则会为空,会抛出异常:There is no PasswordEncoder mapped for id "null"。

好啦,重点来啦,我们现在开始设置自定义登录页面,并从数据库读取账号密码。

 一般来讲,我们先讲认证原理及流程比较好,不过这个地方我也说不太清楚。那我们还是从例子说起吧。

 我用的是 MyBaits 框架操作 Mysql 数据库。为了支持它们,我们需要在原来的 pom.xml 中添加依赖。

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.0</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

好啦,现在我们首先定义一个用户对象,为了简单,我们只有三个属性:id,username,password。

package security.pojo;

public class User {
    private int id;
    private String username;
    private String password;
    private String roles;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getRoles() {
        return roles;
    }
    public void setRoles(String roles) {
        this.roles = roles;
    }

}

然后,为了根据用户名找到用户,我们定义一个 Mapper:

package security.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import security.pojo.User;
@Mapper
public interface UserMapper {
    
    @Select("select * from users where username = #{username}")
    public User findByUsername(String username);

}

而这样的一个 Mapper 是不会加载到 Bean 中去的,我们需要对这个类进行扫描:

package security;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("security.mapper")
public class SecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class, args);
    }

}

好啦,这个 Mapper 已经成为一个 Bean 了,下面的将是重点:来自 《Spring Boot 2 企业应用实战》

1、UserDetails

      UserDetails 是 Spring Security 的一个核心接口。其中定义了一些可以获取用户名、密码、权限等与认证相关信息的方法。

   Spring Security 内部使用的 UserDetails 实现类大都是内置的 User 类,要使用 UserDetails,也可以直接使用该类。

      在 Spring Security 内部,很多需要使用用户信息的时候,基本上都是使用 UserDetails,比如在登录认证的时候。

      UserDetails 是通过 UserDetailsService 的 loadUserByUsername() 方法进行加载的。

      我们也需要实现自己的 UserDetailsService 来加载自定义的 UserDetails 信息。

2、UserDetailsService

      Authentication.getPrincipal() 的返回类型是 Object,但很多情况下返回的其实是一个 UserDetails 的实例。

   登录认证的时候 Spring Security 会通过 UserDetailsService 的 loadByUsername() 方法获取相对应的 UserDetails

      进行认证,认证通过后会将改 UserDetails 赋给认证通过的 Authentication 的 principal,

   然后再把该 Authentication 存入 SecurityContext。之后如果需要使用用户信息,

      可以通过 SecurityContextHolder 获取存放在 SecurityContext 中的 Authentication 的 principal。

3、Authentication

      Authentication 用来表示用户认证信息,在用户登录认证之前,

      Spring Security 会将相关信息封装为一个 Authentication

      具体实现类的对象,在登录认证成功之后又会生成一个信息更全面、包含用户权限等信息的 Authentication 对象,

      然后把它保存在 SpringContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用,如访问权限的鉴定等。

4、SecurityContextHolder

      SecurityContextHolder 是用来保存 SecurityContext 的。SecurityContext 中含有当前所访问系统的用户的详细信息。

      默认情况下,SecurityContextHolder 将使用 ThreadLocal 来保存 SecurityContext。

      这也就意味着在处于同一线程的方法中,可以从 ThreadLocal 获取到当前 SecurityContext。

好啦,这个地方就到这儿啦,没弄懂也不要紧,我们能看懂例子就行了:

package security.service;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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 security.mapper.UserMapper;
import security.pojo.User;

@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserMapper userMapper;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // TODO Auto-generated method stub
        User user = userMapper.findByUsername(username);
        if(user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority(user.getRoles()));
        return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(),authorities);
    }

}

在这个类中,我们实现了 UserDetailsService 接口,然后重写了 loadUserByUsername(String username) 方法。

之后自动注入了一个根据用户名查找用户的 Mapper,再将查找的用户对象复制给 user。

当存在这个用户的时候,我们获取它的权限添加到权限列表中,然后把这个列表以及用户名,密码存入到 UserDetails 对象中。

因为一个用户的权限可能不止一个,所以是一个权限列表。另外由于之前定义的类名叫 User,所以在 return 那个地方需要这么写。

你们可以把 User 类名改成其他的,再 import org.springframework.security.core.userdetails; 然后 return new User(..) 。

 在这个类里有个这个类型 GrantedAuthority:

Authentication 的 getAuthority() 可以返回当前 Authentication 对象所拥有的权限,即当前用户所拥有的权限,

其返回值是一个 GrantedAuthority 类型的数组,每一个 GrantedAuthority 对象代表赋予给当前用户的一种权限。

GrantedAuthority 是一个接口,其通常是通过 UserDetailsService 进行加载,然后赋予 UserDetails 的。

GrantedAuthority 中只定义了一个 getAuthority() 方法,该方法返回一个字符串,表示对应的权限。

如果对应的权限不能用字符串表示,则应当返回 null。

最后我们到了配置环节了:

package security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import security.service.UserService;

@SuppressWarnings("deprecation")
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;
    @Autowired
    private AuthenticationProvider authenticationProvider;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // TODO Auto-generated method stub
        auth.authenticationProvider(authenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // TODO Auto-generated method stub
        http.authorizeRequests()
        .antMatchers("/css/**","/images/*","/js/**","/login").permitAll()
        .antMatchers("/index").hasRole("USER")
        .anyRequest().authenticated()
        .and()
        .formLogin().loginPage("/login")
        .usernameParameter("username")
        .passwordParameter("password")
        .defaultSuccessUrl("/success")
        .failureUrl("/failure");
    }
    
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userService);
        provider.setPasswordEncoder(NoOpPasswordEncoder.getInstance());
        return provider;
    }
    

}

 这个类中我们重写了两个 configure() 方法。其中一个我们之前谈过,不过并没有讲全,现在补充一下:

在 formLogin() 下还有 .usernameParameter() 和 .passwordParameter() 这两个函数。

这两个函数是用于指定登录页面用户名及密码的标识的。

failureUrl 是指定登录失败显示的页面的。

还有其他的一些我们以后用到再讲。

另一个 configure() 方法是用于认证的。我们这里仅仅只写了一行代码。

我们把之前的 @Service 的那个类注入到了 userService 中,再把 @Bean 的那个 Bean 注入到了 authenticationProvider 中。

在这个 Bean 里面有个 DaoAuthenticationProvider 类:

Spring Security 默认会使用 DaoAuthenticationProvider 实现 AuthenticationProvider 接口,专门进行用户认证处理。

DaoAuthenticationProvider 在进行认证处理的时候需要一个 UserDetailsService 来获取用户的信息 UserDetails,

其中包括用户名,密码和所拥有的权限等。

看到这些代码,可以知道我们写的代码都有联系了。我们还差一个控制器的代码:

package security.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class SecurityController {
    
    @RequestMapping("/login")
    public String login() {
        return "login";
    }
    
    @RequestMapping("/success")
    public String success() {
        return "success";
    }
    
    @RequestMapping("/failure")
    public String failure() {
        return "failure";
    }
    
    @RequestMapping("/index")
    public String index() {
        return "index";
    }
    
    

}

好啦,到此 java 代码就结束了,我们就差几个页面没写,这里仅写一个重要的 login 页面作为演示:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
    <form th:action="@{/login}" method="post">
        <input th:name="username" type="text">
        <input th:name="password" type="password">
        <input type="submit" value="login">
    </form>
</body>
</html>

 前面我们设置了 usernameParameter("username"),passwordParameter("password"),

在这里我们用的是 thymeleaf 模板,所以用户名和密码就分别用 th:name="username",th:name="password"。

最后我们看下数据库:

熬,对啦,连接数据库的地方需要写在 application.properties 文件里:

注意了,那个 url 数据库(security)后面一定要写上 ?serverTimezone=UTC&characterEncoding=utf-8 这样的,不然会出错的。

至此,入门项目就结束了,所有的源码都在上面啦,觉得可以的话点个赞啦!

想要源码的话可以点此下载:Download  (CSDN)

或者百度云:

链接:https://pan.baidu.com/s/1-1jhJs1uGUIN7ELAP6V3NQ&shfl=sharepset
提取码:h80y

猜你喜欢

转载自www.cnblogs.com/M-Anonymous/p/11708791.html