Spring Security 入门 (二)

咱们在篇(一)中已经谈到了默认的登陆页面以及默认的登陆帐号和密码。css

在这一篇中咱们将本身定义登陆页面及帐号密码。html

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

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

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

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 类,控制器里面的方法我就不写了,仿照(一)很容易写出来,运行结果大家本身测试吧。spring

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

能够添加用户,并给用户指定权限,它还有其余的方法,咱们之后用到再讲。数据库

在 Password 的地方咱们须要注意了:apache

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 企业应用实战》

一、UserDetails

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

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

      在 Spring Security 内部,不少须要使用用户信息的时候,基本上都是使用 UserDetails,好比在登陆认证的时候。

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

      咱们也须要实现本身的 UserDetailsService 来加载自定义的 UserDetails 信息。

二、UserDetailsService

      Authentication.getPrincipal() 的返回类型是 Object,但不少状况下返回的实际上是一个 UserDetails 的实例。

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

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

   而后再把该 Authentication 存入 SecurityContext。以后若是须要使用用户信息,

      能够经过 SecurityContextHolder 获取存放在 SecurityContext 中的 Authentication 的 principal。

三、Authentication

      Authentication 用来表示用户认证信息,在用户登陆认证以前,

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

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

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

四、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")
.loginProcessingUrl("/login") .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() 以及 .loginProcessingUrl("/login") 这三个函数。

前两个函数是用于指定登陆页面用户名及密码的标识的,后面的一个是用于表单请求的 action 参数。

defaultSuccessUrl 是指定登陆成功显示的页面,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"),

另外因为默认的登陆页面表单请求的 action="/login",用户名参数和密码分别为 "username","password"。

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    private String usernameParameter = "username";
    private String passwordParameter = "password";
    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }
    // ... ... 

}

 若是用 thymeleaf 模板的话,这三个参数就分别用 th:action="{/login}" ,th:name="username",th:name="password"。

如果咱们想自定义的话,好比登陆页面为 signin.html,登陆请求的 action 为 "/signin",

用户名参数为 uname,密码参数为 pwd。

    @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("uname")
        .passwordParameter("pwd")
        .loginProcessingUrl("/sign")
        .defaultSuccessUrl("/success")
        .failureUrl("/failure");
    }

<!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="@{/signin}" method="post">
        <input th:name="uname" type="text">
        <input th:name="pwd" type="password">
        <input type="submit" value="login">
    </form>
</body>
</html>

 

最后咱们看下数据库:

 

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

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

 

至此,入门项目就结束了,全部的源码都在上面啦,以为能够的话点个赞啦!

连接:https://pan.baidu.com/s/1X1kTs6OpyidZv_627Xadiw&shfl=sharepset 提取码:jexz

相关文章
相关标签/搜索