Spring Security
Spring Security
2020/02/05 (微調內容)
2020/04/30 (新增連結)
基本概念
Spring提供一個相當完整且簡單的登入驗證機制,可以將帳號密碼寫在程式中(in memory)的方式進行,也可以將帳號密碼儲存在資料庫(詳參:http://docs.spring.io/spring-security/site/docs/4.2.2.RELEASE/reference/htmlsingle/#jc-authentication-jdbc),也可以跟Lightweight Directory Access Protocol (LDAP)(詳參:http://docs.spring.io/spring-security/site/docs/4.2.2.RELEASE/reference/htmlsingle/#ldap-authentication、https://projects.spring.io/spring-ldap/)或OAuth(詳參:https://projects.spring.io/spring-security-oauth/)整合。
設定
pom.xml (in memory)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
pom.xml (jdbc)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
application.properties (同jdbc的設定)
spring.datasource.url=jdbc:mysql://localhost/practice?verifyServerCertificate=false&useSSL=false&requireSSL=false
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
資料表: (開了兩個帳號:admin、user,並設定admin為ROLE_ADMIN及ROLE_USER,設定user為ROLE_ADMIN及ROLE_USER,詳參: Security Database Schema)
create table users(
username varchar(50) not null primary key,
password varchar(50) not null,
enabled boolean not null
);
create table authorities (
username varchar(50) not null,
authority varchar(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);
INSERT INTO `users` VALUES ('admin','password',1),('user','password',1);
INSERT INTO `authorities` VALUES ('admin','ROLE_ADMIN'),('admin','ROLE_USER'),('user','ROLE_USER');
Security in Memory範例
將帳號密碼寫在程式中 (SecurityConfig.java),首先在configure()中先利用antMatchers()設定「/css/**」(該目錄下所有檔案)及「index」不需要認證以及「/user/**」(該目錄下所有檔案)需要有「USER」的角色,「/admin/**」(該目錄下所有檔案)需要有「ADMIN」的角色。
http
.authorizeRequests()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
再利用.formLogin()設定登入的view及登入失敗的URL。如果有權限錯誤時,重導到「/」。
.formLogin()
.loginPage("/login").failureUrl("/login-error")
.and()
.exceptionHandling().accessDeniedPage("/");
在configureGlobal()中利用inMemoryAuthentication()設定使用者的帳號(如:user、admin)、密碼(如:password、password)及角色(如:USER、ADMIN)。
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
.and()
.withUser("admin").password("password").roles("ADMIN");
完整的程式:
package com.example.demo;
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 {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/login").failureUrl("/login-error")
.and()
.exceptionHandling().accessDeniedPage("/");
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
.and()
.withUser("admin").password("password").roles("ADMIN");
}
}
對應的controller (MainController.java)
package com.example.demo.controller;
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author Joe Grandja
*/
@Controller
public class MainController {
@RequestMapping("/")
public String root() {
return "redirect:/index";
}
@RequestMapping("/index")
public String index() {
return "index";
}
@RequestMapping("/user/index")
public String userIndex() {
return "user/index";
}
@RequestMapping("/admin/index")
public String adminIndex() {
return "admin/index";
}
@RequestMapping("/login")
public String login() {
return "login";
}
@RequestMapping("/login-error")
public String loginError(Model model) {
model.addAttribute("loginError", true);
return "login";
}
}
對應的view:
index.html (src/main/resources/templates/index.html)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<title>Hello Spring Security</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" />
</head>
<body>
<div th:fragment="logout" class="logout" sec:authorize="isAuthenticated()">
Logged in user: <span sec:authentication="name"></span> |
Roles: <span sec:authentication="principal.authorities"></span>
<div>
<form action="#" th:action="@{/logout}" method="post">
<input type="submit" value="Logout" />
</form>
</div>
</div>
<h1>Hello Spring Security</h1>
<p>This is an unsecured page, but you can access the secured pages after authenticating.</p>
<ul>
<li>Go to the <a href="/user/index" th:href="@{/user/index}">secured pages for users</a></li>
<li>Go to the <a href="/admin/index" th:href="@{/admin/index}">secured pages for administrators</a></li>
</ul>
</body>
</html>
login.html (src/main/resources/templates/login.html)
<!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" />
<link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" />
</head>
<body>
<h1>Login page</h1>
<p th:if="${loginError}" class="error">Wrong user or password</p>
<form th:action="@{/login}" method="post">
<label for="username">Username</label>:
<input type="text" id="username" name="username" autofocus="autofocus" /><br/>
<label for="password">Password</label>:
<input type="password" id="password" name="password" /> <br />
<input type="submit" value="Log in" />
</form>
<p><a href="/index" th:href="@{/index}">Back to home page</a></p>
</body>
</html>
user/index.html (src/main/resources/templates/user/index.html)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Hello Spring Security</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" />
</head>
<body>
<div th:substituteby="index::logout"></div>
<h1>This is a secured page for user!</h1>
<p><a href="/index" th:href="@{/index}">Back to home page</a></p>
</body>
</html>
admin/index.html (src/main/resources/templates/admin/index.html)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Hello Spring Security</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" />
</head>
<body>
<div th:substituteby="index::logout"></div>
<h1>This is a secured page for administrator!</h1>
<p><a href="/index" th:href="@{/index}">Back to home page</a></p>
</body>
</html>
css/main.css (src/main/resources/static/css/main.css)
body {
font-family: sans;
font-size: 1em;
}
p.error {
font-weight: bold;
color: red;
}
div.logout {
float: right;
}
Security with JDBC範例
SecurityConfig.java只要把auth.inMemoryAuthentication()改為auth.jdbcAuthentication().dataSource(dataSource)並將dataSource設為Autowired就大功告成了!!
package com.example.demo;
import javax.sql.DataSource;
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 {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/login").failureUrl("/login-error")
.and()
.exceptionHandling().accessDeniedPage("/");
}
@Autowired
private DataSource dataSource;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource);
}
}
如果不要使用default schema,例如,資料表的名稱或欄位名稱如果有所不同的時候:
create table users(
username varchar(50) not null primary key,
password varchar(50) not null,
enabled boolean not null
);
create table user_roles (
username varchar(50) not null,
role varchar(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on user_roles (username,role);
INSERT INTO `users` VALUES ('admin','password',1),('user','password',1);
INSERT INTO `user_roles` VALUES ('admin','ROLE_ADMIN'),('admin','ROLE_USER'),('user','ROLE_USER');
可以改成:
package com.example.demo;
import javax.sql.DataSource;
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 {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/login").failureUrl("/login-error")
.and()
.exceptionHandling().accessDeniedPage("/");
}
@Autowired
private DataSource dataSource;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select username,password, enabled from users where username=?")
.authoritiesByUsernameQuery(
"select username, role from user_roles where username=?");
}
}
密碼加密
Spring Security 5要求所有密碼都要加密:
兩(多)個登入介面或資料表
Two Login Pages with Spring Security
Configuring 2 Http Elements
Custom Login Pages
Authentication Configuration
Using a Common User Authentication Source
Using Two Different User Authentication Sources
Multiple Entry Points in Spring Security
利用Order決定不同設定的優先順序
利用http.antMatcher("/admin/**")來指定哪些網頁使用這個設定
@Configuration
@Order(1)
public static class App1ConfigurationAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/admin/**")
.authorizeRequests().anyRequest().hasRole("ADMIN")
.and().httpBasic().authenticationEntryPoint(authenticationEntryPoint());
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint(){
BasicAuthenticationEntryPoint entryPoint =
new BasicAuthenticationEntryPoint();
entryPoint.setRealmName("admin realm");
return entryPoint;
}
}
REST
Securing a Rest API With Spring Security
The following Spring security setup works as following:
The user logs in with a POST request containing his username and password,
The server returns a temporary / permanent authentication token,
The user sends the token within each HTTP request via an HTTP header Authorization: Bearer TOKEN.
Now, let’s see different examples with variety of authentications:
Simple Example: authentication based on the UUID of the user,
JWT Example: authentication based on a JWT token.
Integrating Spring Boot and React with Spring Security - Basic and JWT Authentication
HTTPS
常見問題
為什麼會有為什麼會有「HTTP 403 - Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'」錯誤呢?
CSRF是個常見的網頁攻擊 (詳見: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)),spring security的預設值是會打開對於這個攻擊的保護的預設值是會打開對於這個攻擊的保護,保護的方式是會加入保護的方式是會加入一個token。當我們使用th:action時,Thymeleaf會自動加上token。所以,如果沒有使用th:action時,就會有以上的錯誤。
<form th:action="@{/login}" method="post">
<label for="username">Username</label>:
<input type="text" id="username" name="username" autofocus="autofocus" /><br/>
<label for="password">Password</label>:
<input type="password" id="password" name="password" /> <br />
<input type="submit" value="Log in" />
</form>
要如何客製無法存取的頁面?
https://www.mkyong.com/spring-security/customize-http-403-access-denied-page-in-spring-security/
如何在controller裡取得登入者的帳號?
建議採用以下方法:
import java.security.Principal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class LoginController {
@RequestMapping(value="/login", method = RequestMethod.GET)
public String printWelcome(Model model, Principal principal) {
String name = principal.getName(); //get logged in username
model.addAttribute("username", name);
return "hello";
}
https://www.mkyong.com/spring-security/get-current-logged-in-username-in-spring-security/
為什麼我的logout不能用?
參考資料
The Registration Series
The Authentication Series
Core Spring Security
Spring Security with REST
http://docs.spring.io/spring-security/site/docs/4.2.2.RELEASE/guides/html5/helloworld-boot.html
http://docs.spring.io/spring-security/site/docs/4.2.2.RELEASE/guides/html5/hellomvc-javaconfig.html
http://docs.spring.io/spring-security/site/docs/4.2.2.RELEASE/reference/htmlsingle/
https://docs.spring.io/spring-security/site/docs/4.2.x/reference/html/appendix-schema.html
http://www.programming-free.com/2015/09/spring-security-jdbc-authentication.html
http://www.programming-free.com/2016/01/spring-security-spring-data-jpa.html