HelloWood

Spring Boot 中集成 Spring Security

2018-01-01

Spring Boot 集成 Spring Security的简单应用,从数据库读取数据校验用户,页面使用Thymeleaf模板


项目地址 https://github.com/helloworlde/SpringSecurity

演示 http://project.hellowood.com.cn/Security/

创建 Spring Boot 应用

添加依赖

1
2
3
4
5
6
7
8
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.0')
compile('org.springframework.boot:spring-boot-starter-thymeleaf')
runtime('mysql:mysql-connector-java')
runtime('org.springframework.boot:spring-boot-starter-tomcat')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.springframework.security:spring-security-test')

创建用户表并插入数据

1
2
3
4
5
6
7
8
9
CREATE TABLE user (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(45) NOT NULL,
password VARCHAR(45) NOT NULL,
enabled INT NOT NULL DEFAULT 1
);


INSERT INTO user (username, password, enabled) VALUES ('username', 'password', TRUE);

添加配置信息

1
2
3
4
5
6
spring.datasource.url=jdbc:mysql://localhost:3306/security?useSSL=false
spring.datasource.username=security
spring.datasource.password=security
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
mybatis.type-aliases-package=cn.com.hellowood.springsecurity.mapper
mybatis.mapper-locations=mappers/**Mapper.xml

添加 Security 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

import cn.com.hellowood.springsecurity.security.CustomAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
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;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;

@Override
protected void configure(HttpSecurity http) throws Exception {
// 所有请求均可访问
http.authorizeRequests()
.antMatchers("/", "/login", "/login-error", "/css/**", "/index")
.permitAll();

// 其余所有请求均需要权限
http.authorizeRequests()
.anyRequest()
.authenticated();

// 配置登录页面的表单 action 必须是 '/login', 用户名和密码的参数名必须是 'username' 和 'password',
// 登录失败的 url 是 '/login-error'
http.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("password")
.failureUrl("/login-error");
}

/**
* Configure global.
*
* @param auth the auth
* @throws Exception the exception
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
// 使用自定义的 Authentication Provider
auth.authenticationProvider(customAuthenticationProvider);
}
}

添加自定义的 Authentication Provider 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import cn.com.hellowood.springsecurity.model.UserModel;
import cn.com.hellowood.springsecurity.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

private final Logger logger = LoggerFactory.getLogger(getClass());

@Autowired
private HttpSession session;

@Autowired
private UserService userService;

/**
* Validate user info is correct form database
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();

// 检查用户名密码是否正确
UserModel user = userService.loadUserByUsernameAndPassword(username, password);
if (user == null) {
logger.error("{} login failed, username or password is wrong", username);
throw new BadCredentialsException("Username or password is not correct");
} else if (!user.getEnabled()) {
throw new AccountExpiredException("Account had expired");
}

// 用户信息有效时将其放入 session 中
session.setAttribute("user", user);
Authentication auth = new UsernamePasswordAuthenticationToken(username, password, grantedAuthorities);
return auth;
}


@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}

}

添加校验用户信息所需要的类

  • 添加 UserModel.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114

    public class UserModel {

    private Integer id;

    private String username;

    private String password;

    private Boolean enabled;

    /**
    * Instantiates a new User model.
    */
    public UserModel() {
    }

    /**
    * Instantiates a new User model.
    *
    * @param id the id
    * @param username the username
    * @param password the password
    * @param enabled the enabled
    */
    public UserModel(Integer id, String username, String password, Boolean enabled) {
    this.id = id;
    this.username = username;
    this.password = password;
    this.enabled = enabled;
    }

    /**
    * Gets id.
    *
    * @return the id
    */
    public Integer getId() {
    return id;
    }

    /**
    * Sets id.
    *
    * @param id the id
    */
    public void setId(Integer id) {
    this.id = id;
    }

    /**
    * Gets username.
    *
    * @return the username
    */
    public String getUsername() {
    return username;
    }

    /**
    * Sets username.
    *
    * @param username the username
    */
    public void setUsername(String username) {
    this.username = username;
    }

    /**
    * Gets password.
    *
    * @return the password
    */
    public String getPassword() {
    return password;
    }

    /**
    * Sets password.
    *
    * @param password the password
    */
    public void setPassword(String password) {
    this.password = password;
    }

    /**
    * Gets enabled.
    *
    * @return the enabled
    */
    public Boolean getEnabled() {
    return enabled;
    }

    /**
    * Sets enabled.
    *
    * @param enabled the enabled
    */
    public void setEnabled(Boolean enabled) {
    this.enabled = enabled;
    }

    @Override
    public String toString() {
    return "UserModel{" +
    "id=" + id +
    ", username='" + username + '\'' +
    ", password='" + password + '\'' +
    ", enabled=" + enabled +
    '}';
    }
    }
  • 添加 UserService.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    import cn.com.hellowood.springsecurity.mapper.UserMapper;
    import cn.com.hellowood.springsecurity.model.UserModel;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    @Service("userService")
    public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
    * Load user by username and password user model.
    *
    * @param username the username
    * @param password the password
    * @return the user model
    */
    public UserModel loadUserByUsernameAndPassword(String username, String password) {
    return userMapper.getUserByUsernameAndPassword(username, password);
    }
    }
  • 添加 UserMapper.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import cn.com.hellowood.springsecurity.model.UserModel;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;

    @Mapper
    public interface UserMapper {

    /**
    * Gets user by username and password.
    *
    * @param username the username
    * @param password the password
    * @return the user by username and password
    */
    UserModel getUserByUsernameAndPassword(@Param("username") String username,
    @Param("password") String password);
    }
  • 添加 UserMapper.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?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="cn.com.hellowood.springsecurity.mapper.UserMapper">

    <resultMap id="baseResultMap" type="cn.com.hellowood.springsecurity.model.UserModel">
    <id column="id" property="id" javaType="java.lang.Integer" jdbcType="INTEGER"></id>
    <result column="username" property="username" javaType="java.lang.String" jdbcType="VARCHAR"></result>
    <result column="password" property="password" javaType="java.lang.String" jdbcType="VARCHAR"></result>
    <result column="enabled" property="enabled" javaType="java.lang.Boolean" jdbcType="INTEGER"></result>
    </resultMap>

    <select id="getUserByUsernameAndPassword" resultType="cn.com.hellowood.springsecurity.model.UserModel">
    SELECT
    id,
    username,
    password,
    enabled
    FROM user
    WHERE username = #{username, jdbcType=VARCHAR}
    AND password = #{password, jdbcType=VARCHAR}
    </select>
    </mapper>

    添加页面

  • index.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head>
    <title>Spring Security</title>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
    <link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}"/>
    <link rel="stylesheet" href="/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}"/>
    </head>
    <body>
    <div class="container">
    <form action="#" class="form-signin">
    <h2 class="form-signin-heading">Hello Spring Security</h2>
    <h5 class="form-signin-heading content-adjust">Anyone can access this page</h5>
    <div th:if="${session.user} != null">
    <h5 class="form-signin-heading content-adjust">Your username is <span th:text="${session.user.username}"></span></h5>
    <a href="/user/index" th:href="@{/user/index}" class="btn btn-success btn-block">To Security page</a>
    </div>
    <div th:if="${session.user} == null">
    <a href="/index" th:href="@{/login}" class="btn btn-primary btn-block">To Login page</a>
    </div>
    </form>
    <div th:fragment="logout" class="logout" th:if="${session.user} != null">
    <form action="#" th:action="@{/logout}" method="post" class="form-signin">
    <button class="btn btn-warning btn-block" type="submit">Log out</button>
    </form>
    </div>
    </div>
    </body>
    </html>
  • login.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head>
    <title>Login page</title>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
    <link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}"/>
    <link rel="stylesheet" href="/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}"/>
    </head>
    <body>
    <div class="container">
    <form th:action="@{/login}" method="post" class="form-signin">
    <h2 class="form-signin-heading">Please sign in</h2>
    <div>
    <label for="username" class="sr-only">Username</label>
    <input type="text" id="username" name="username"
    th:class="${loginError} ? 'form-control is-invalid' : 'form-control'" placeholder="Username"
    required="required"
    autofocus="autofocus"/>
    <div class="invalid-feedback" th:if="${loginError}">
    Wrong username or password
    </div>
    </div>
    <div>
    <label for="password" class="sr-only">Password</label>
    <input type="password" id="password" name="password" class="form-control" placeholder="Password"
    required="required"/>
    </div>
    <button class="btn btn-success btn-block" type="submit">Sign in</button>
    <a href="/index" th:href="@{/index}" class="btn btn-primary btn-block">Back to Home page</a>
    </form>
    </div>
    </body>
    </html>
  • user/index.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head>
    <title>Spring Security</title>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
    <link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}"/>
    <link rel="stylesheet" href="/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}"/>
    </head>
    <body>
    <div class="container">
    <form action="#" class="form-signin">
    <h2 class="form-signin-heading">Hello Spring Security</h2>
    <h5 class="form-signin-heading content-adjust">Only logged in user can access this page</h5>
    <div th:if="${session.user} != null">
    <h5 class="form-signin-heading content-adjust">Logged user is <span th:text="${session.user.username}"></span></h5>
    <a href="/index" th:href="@{/index}" class="btn btn-primary btn-block">Back to Home page</a>
    </div>
    </form>
    <div th:substituteby="index::logout"></div>
    </div>
    </body>
    </html>

添加 Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class MainController {

/**
* Root page.
*
* @return the index page url
*/
@RequestMapping("/")
public String root() {
return "redirect:/index";
}

/**
* Index page.
*
* @return the index page url
*/
@RequestMapping("/index")
public String index() {
return "index";
}

/**
* User index page.
*
* @return the user index page url
*/
@RequestMapping("/user/index")
public String userIndex() {
return "user/index";
}

/**
* Login page.
*
* @return the login page url
*/
@RequestMapping("/login")
public String login() {
return "login";
}

/**
* Login error page.
*
* @param model the model
* @return the login error page url
*/
@RequestMapping("/login-error")
public String loginError(Model model) {
model.addAttribute("loginError", true);
return "login";
}

}

启动应用,访问http://localhost:8080/user/index,此时没有登录,会被拦截并重定向到登录页面http://localhost:8080/login,输入用户名 username 和密码 password,登录成功后再次访问http://localhost:8080/user/index,此时该 url 可以正常访问,当输入错误的用户名或密码时会提示错误信息,说明 Spring Security 配置正确