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-authenticationhttps://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要求所有密碼都要加密:

兩(多)個登入介面或資料表

@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

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不能用?

請詳參: Spring Security logout does not work - does not clear security context and authenticated user still exists

參考資料