SpringBoot安全認(rèn)證Security的實(shí)現(xiàn)方法
一、基本環(huán)境搭建
父pom依賴
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> </parent>
1. 添加pom依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
2. 創(chuàng)建測(cè)試用Controller
@RestController
public class TestController {
@GetMapping("getData")
public String getData() {
return "date";
}
}
3. 創(chuàng)建SpringBoot啟動(dòng)類并run
@SpringBootApplication
public class SpringBootTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootTestApplication.class, args);
}
}
4. 測(cè)試
訪問(wèn)http://127.0.0.1:8080/getData,由于我們開(kāi)啟了SpringSecurity且當(dāng)前是未登錄狀態(tài),頁(yè)面會(huì)被302重定向到http://127.0.0.1:8080/login,頁(yè)面如下:

用戶名:user,密碼可以在控制臺(tái)輸出中找到:

輸入正確的用戶名和密碼后點(diǎn)擊Login按鈕即被重新302到http://127.0.0.1:8080/getData并顯示查詢數(shù)據(jù):

這表示我們的接口已經(jīng)被spring保護(hù)了。
那么肯定會(huì)有小伙伴吐槽了,這么復(fù)雜的密碼,鬼才及得住,所以...
二、為Spring Security設(shè)定用戶名和密碼
為了解決復(fù)雜密碼的問(wèn)題,我們可以在application.yml中做如下設(shè)定:
spring: security: user: name: user password: 123
這樣我們就可以通過(guò)用戶名user密碼123來(lái)訪問(wèn)http://127.0.0.1:8080/getData接口了。
然后肯定又有小伙伴吐槽了,整個(gè)系統(tǒng)就一個(gè)用戶么?有哪個(gè)系統(tǒng)是只有一個(gè)用戶的?所以...
三、為Spring Security設(shè)定多個(gè)用戶
如果想要給Spring Security設(shè)定多個(gè)用戶可用,則新建一個(gè)class,實(shí)現(xiàn)接口WebMvcConfigurer(注意:springBoot版本2.0以上,jdk1.8以上):
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("admin").password("admin").roles("").build());
manager.createUser(User.withUsername("guest").password("guest").roles("").build());
return manager;
}
}
注意需要注解@EnableWebSecurity
InMemoryUserDetailsManager:顧名思義,將用戶名密碼存儲(chǔ)在內(nèi)存中的用戶管理器。我們通過(guò)這個(gè)管理器增加了兩個(gè)用戶,分別是:用戶名admin密碼admin,用戶名guest密碼guest。
做完如上更改后重啟應(yīng)用,再次訪問(wèn)http://127.0.0.1:8080/getData,輸入admin/admin或guest/guest即可通過(guò)身份驗(yàn)證并正常使用接口了。
看到這肯定又有小伙伴要吐槽了:用戶數(shù)據(jù)直接硬編碼到代碼里是什么鬼!我要把用戶放在數(shù)據(jù)庫(kù)!所以...
四、SpringSecurity+Mysql
想要使用數(shù)據(jù)庫(kù),那么我們可以
1. 增加如下依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
2. 配置數(shù)據(jù)庫(kù)連接
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.168.2.12:3306/test?characterEncoding=utf8 username: root password: onceas
3. 創(chuàng)建測(cè)試用表結(jié)構(gòu)及數(shù)據(jù)
drop table if exists test.user; create table test.user ( id int auto_increment primary key, username varchar(50), password varchar(50) ); insert into test.user(id, username, password) values (1, 'admin', 'admin'); insert into test.user(id, username, password) values (2, 'guest', 'guest');
我們創(chuàng)建了用戶信息表,并插入兩個(gè)用戶信息,用戶名/密碼依然是admin/admin、guest/guest
4. entity、dao、service
public class User {
private int id;
private String username;
private String password;
// get set ...
}
@Repository
public class LoginDao {
private final JdbcTemplate jdbcTemplate;
@Autowired
public LoginDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<User> getUserByUsername(String username) {
String sql = "select id, username, password from user where username = ?";
return jdbcTemplate.query(sql, new String[]{username}, new BeanPropertyRowMapper<>(User.class));
}
}
@Service
public class LoginService {
private final LoginDao loginDao;
@Autowired
public LoginService(LoginDao loginDao) {
this.loginDao = loginDao;
}
public List<User> getUserByUsername(String username) {
return loginDao.getUserByUsername(username);
}
}
5. 調(diào)整WebSecurityConfig
@Bean
public UserDetailsService userDetailsService() {
return username -> {
List<UserEntity> users = loginService.getUserByUsername(username);
if (users == null || users.size() == 0) {
throw new UsernameNotFoundException("用戶名未找到");
}
String password = users.get(0).getPassword();
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String passwordAfterEncoder = passwordEncoder.encode(password);
return User.withUsername(username).password(passwordAfterEncoder).roles("").build();
};
}
做完如上更改后重啟應(yīng)用,再次訪問(wèn)http://127.0.0.1:8080/getData,輸入admin/admin或guest/guest即可通過(guò)身份驗(yàn)證并正常使用接口了。
關(guān)于UserDetailsService,有些東西要說(shuō)明下:
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); String passwordAfterEncoder = passwordEncoder.encode(password);
上面這兩句代碼是在對(duì)用戶密碼進(jìn)行加密。為什么要這樣子呢?看到這肯定又有小伙伴會(huì)吐槽:數(shù)據(jù)庫(kù)存儲(chǔ)銘文密碼是什么鬼!對(duì),Spring也是盡量在幫助開(kāi)發(fā)者避免這個(gè)事情。所以SpringSecurity在進(jìn)行密碼比對(duì)的時(shí)候需要開(kāi)發(fā)者提供加密后的密碼。我們上面的寫法其實(shí)是不合理的,實(shí)際情況應(yīng)該是數(shù)據(jù)庫(kù)中存儲(chǔ)密文密碼,然后將數(shù)據(jù)庫(kù)中的密碼直接傳給User.password()就可以了。
6. 關(guān)于SpringSecurity加密后的密文格式
我們可以通過(guò)打斷點(diǎn)的方式或者增加
System.out.println(username + "---->>>" + passwordAfterEncoder);
來(lái)查看下,如果admin/admin被登錄時(shí)候,passwordAfterEncoder的值是什么?輸出結(jié)果:
admin---->>>{bcrypt}$2a$10$d4VkiIfP7MyNSipjLtQ0Keva4ST6U6Fnw77iiv39IGnGswptqWRG.
guest---->>>{bcrypt}$2a$10$8jRMbiGzFIS4GU3SWAm83eWgFO29EEb5QhXOEkPEaabw5Oiy/jxUC
可以看出加密后的密碼可以分為兩部分
{}內(nèi)描述了加密算法,這里為bcrypt算法。{}后面即為密文密碼,這里是包含鹽的。
所以SpringSecurity的工作原理就是:當(dāng)用戶輸入用戶名和密碼點(diǎn)擊Login以后,SpringSecurity先通過(guò)調(diào)用我們自定義的UserDetailsService獲取到加密后密碼,然后根據(jù){}里的內(nèi)容獲知加密算法,再將用戶輸入的密碼按照該算法進(jìn)行加密,最后再與{}后的密文密碼比對(duì)即可獲知用戶憑據(jù)是否有效。
通過(guò)查看PasswordEncoderFactories的源碼,我們可以知道SpringEncoder工具可以提供哪些加密算法:
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
其中LdapShaPasswordEncoder、Md4PasswordEncoder、MessageDigestPasswordEncoder、NoOpPasswordEncoder、StandardPasswordEncoder已經(jīng)不建議使用了。SpringSecurity認(rèn)為:
Digest based password encoding is not considered secure. //基于摘要的密碼編碼被認(rèn)為是不安全的
五 、權(quán)限控制
以上內(nèi)容我們只解決了用戶登錄問(wèn)題,但是實(shí)際開(kāi)發(fā)中僅僅完成用戶登錄是不夠的,我們還需要用戶授權(quán)及授權(quán)驗(yàn)證。由于我們已經(jīng)將用戶信息存儲(chǔ)到數(shù)據(jù)庫(kù)里了,那么姑且我們也將權(quán)限信息存儲(chǔ)在數(shù)據(jù)庫(kù)吧。
1. 準(zhǔn)備數(shù)據(jù)庫(kù)表及測(cè)試數(shù)據(jù)
drop table if exists test.role; create table test.role ( id int auto_increment primary key, role varchar(50) ); drop table if exists test.permission; create table test.permission ( id int auto_increment primary key, permission varchar(50) ); drop table if exists test.user_r_role; create table test.user_r_role ( userid int, roleid int ); drop table if exists test.role_r_permission; create table test.role_r_permission ( roleid int, permissionid int ); drop table if exists test.user_r_permission; create table test.user_r_permission ( userid int, permissionid int ); insert into test.role(id, role) values (1, 'adminRole'); insert into test.role(id, role) values (2, 'guestRole'); insert into test.permission(id, permission) values (1, 'permission1'); insert into test.permission(id, permission) values (2, 'permission2'); insert into test.permission(id, permission) values (3, 'permission3'); insert into test.permission(id, permission) values (4, 'permission4'); insert into test.user_r_role(userid, roleid) values (1, 1); insert into test.user_r_role(userid, roleid) values (2, 2); insert into test.role_r_permission(roleid, permissionid) values (1, 1); insert into test.role_r_permission(roleid, permissionid) values (1, 2); insert into test.user_r_permission(userid, permissionid) values (1, 3); insert into test.user_r_permission(userid, permissionid) values (1, 4); insert into test.user_r_permission(userid, permissionid) values (2, 3); insert into test.user_r_permission(userid, permissionid) values (2, 4);
- role:角色信息表,permission權(quán)限信息表,user_r_role用戶所屬角色表,role_r_permission角色擁有權(quán)限表,user_r_permission用戶擁有權(quán)限表。
- 由于用戶有所屬角色且角色是有權(quán)限的,用戶同時(shí)又單獨(dú)擁有權(quán)限,所以用戶最終擁有的權(quán)限取并集。
- 用戶admin最終擁有角色adminRole以及權(quán)限:permission1、permission2、permission3、permission4
- 用戶guest最終擁有角色guestRole以及權(quán)限:permission3、permission4
2. Dao、Service
dao增加方法:根據(jù)用戶名查角色以及根據(jù)用戶名查權(quán)限
public List<String> getPermissionsByUsername(String username) {
String sql =
"select d.permission\n" +
"from user a\n" +
" join user_r_role b on a.id = b.userid\n" +
" join role_r_permission c on b.roleid = c.roleid\n" +
" join permission d on c.permissionid = d.id\n" +
"where a.username = ?\n" +
"union\n" +
"select c.permission\n" +
"from user a\n" +
" join user_r_permission b on a.id = b.userid\n" +
" join permission c on b.permissionid = c.id\n" +
"where a.username = ?";
return jdbcTemplate.queryForList(sql, new String[]{username, username}, String.class);
}
public List<String> getRoleByUsername(String username) {
String sql =
"select c.role\n" +
"from user a\n" +
" join user_r_role b on a.id = b.userid\n" +
" join role c on b.roleid = c.id\n" +
"where a.username = ?";
return jdbcTemplate.queryForList(sql, new String[]{username}, String.class);
}
service增加方法:根據(jù)用戶名查角色以及根據(jù)用戶名查權(quán)限
public List<String> getPermissionsByUsername(String username) {
return loginDao.getPermissionsByUsername(username);
}
public List<String> getRoleByUsername(String username) {
return loginDao.getRoleByUsername(username);
}
3. WebSecurityConfig
(1)調(diào)整public UserDetailsService userDetailsService()方法,在構(gòu)建用戶信息的時(shí)候把用戶所屬角色和用戶所擁有的權(quán)限也填充上(最后return的時(shí)候)。
@Bean
public UserDetailsService userDetailsService() {
return username -> {
List<UserEntity> users = loginService.getUserByUsername(username);
if (users == null || users.size() == 0) {
throw new UsernameNotFoundException("用戶名未找到");
}
String password = users.get(0).getPassword();
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String passwordAfterEncoder = passwordEncoder.encode(password);
System.out.println(username + "/" + passwordAfterEncoder);
List<String> roles = loginService.getRoleByUsername(username);
List<String> permissions = loginService.getPermissionsByUsername(username);
String[] roleArr = new String[roles.size()];
String[] permissionArr = new String[permissions.size()];
return User.withUsername(username).password(passwordAfterEncoder).
roles(roles.toArray(roleArr)).authorities(permissions.toArray(permissionArr)).
build();
};
}
這里面有個(gè)坑,就是紅色代碼部分。具體可查看org.springframework.security.core.userdetails.User.UserBuilder。roles()方法和authorities()方法實(shí)際上都是在針對(duì)UserBuilder的authorities屬性進(jìn)行set操作,執(zhí)行roles("roleName")和執(zhí)行authorities("ROLE_roleName")是等價(jià)的。所以上例代碼中roles(roles.toArray(roleArr))起不到任何作用,直接被后面的authorities(permissions.toArray(permissionArr))覆蓋掉了。
所以正確的寫法可參考:
@Bean
public UserDetailsService userDetailsService() {
return username -> {
List<UserEntity> users = loginService.getUserByUsername(username);
if (users == null || users.size() == 0) {
throw new UsernameNotFoundException("用戶名未找到");
}
String password = users.get(0).getPassword();
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String passwordAfterEncoder = passwordEncoder.encode(password);
System.out.println(username + "/" + passwordAfterEncoder);
List<String> roles = loginService.getRoleByUsername(username);
List<String> permissions = loginService.getPermissionsByUsername(username);
String[] permissionArr = new String[roles.size() + permissions.size()];
int permissionArrIndex = 0;
for (String role : roles) {
permissionArr[permissionArrIndex] = "ROLE_" + role;
permissionArrIndex++;
}
for (String permission : permissions) {
permissionArr[permissionArrIndex] = permission;
permissionArrIndex++;
}
return User.withUsername(username).password(passwordAfterEncoder).authorities(permissionArr).build();
};
}
(2)增加新的bean,為我們需要的保護(hù)的接口設(shè)定需要權(quán)限驗(yàn)證:
@Bean
public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() {
return new WebSecurityConfigurerAdapter() {
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.
authorizeRequests().antMatchers("/guest/**").permitAll().
and().authorizeRequests().antMatchers("/admin/**").hasRole("admin").
and().authorizeRequests().antMatchers("/authenticated/**").authenticated().
and().authorizeRequests().antMatchers("/permission1/**").hasAuthority("permission1").
and().authorizeRequests().antMatchers("/permission2/**").hasAuthority("permission2").
and().authorizeRequests().antMatchers("/permission3/**").hasAuthority("permission3").
and().authorizeRequests().antMatchers("/permission4/**").hasAuthority("permission4").
and().formLogin().
and().authorizeRequests().anyRequest().permitAll();
}
};
}
- /guest/**的接口會(huì)被允許所有人訪問(wèn),包括未登錄的人。
- /admin/**的接口只能被擁有admin角色的用戶訪問(wèn)。
- /authenticated/**的接口可以被所有已經(jīng)登錄的用戶訪問(wèn)。
- /permission1/**的接口可以被擁有permission1權(quán)限的用戶訪問(wèn)。/permission2/**、/permission3/**、/permission4/**同理
4. TestController
最后我們調(diào)整下TestContrller,增加幾個(gè)接口以便測(cè)試:
@RestController
public class TestController {
@GetMapping("getData")
public String getData() {
return "date";
}
@GetMapping("authenticated/getData")
public String getAuthenticatedData() {
return "authenticatedData";
}
@GetMapping("admin/getData")
public String getAdminData() {
return "adminData";
}
@GetMapping("guest/getData")
public String getGuestData() {
return "guestData";
}
@GetMapping("permission1/getData")
public String getPermission1Data() {
return "permission1Data";
}
@GetMapping("permission2/getData")
public String getPermission2Data() {
return "permission2Data";
}
@GetMapping("permission3/getData")
public String getPermission3Data() {
return "permission3Data";
}
@GetMapping("permission4/getData")
public String getPermission4Data() {
return "permission4Data";
}
}
5. 測(cè)試
- 訪問(wèn)/guest/getData無(wú)需登錄即可訪問(wèn)成功。
- 訪問(wèn)/authenticated/getData,會(huì)彈出用戶登錄頁(yè)面。登錄任何一個(gè)用戶都可訪問(wèn)成功。
- 訪問(wèn)/admin/getData,會(huì)彈出用戶登錄頁(yè)面。登錄admin用戶訪問(wèn)成功,登錄guest用戶會(huì)發(fā)生錯(cuò)誤,403未授權(quán)。
- 其他的就不再贅述了。
六、自定義登錄頁(yè)面
是不是覺(jué)得SpringScurity的登錄頁(yè)面丑爆了?是不是想老子還能做一個(gè)更丑的登錄頁(yè)面你信不信?接下來(lái)我們來(lái)弄一個(gè)更丑的登錄頁(yè)面。
1. 增加pom依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
2. 編寫自己的登錄頁(yè)面
thymeleaf默認(rèn)的頁(yè)面放置位置為:classpath:templates/ 目錄下,所以在編寫代碼的時(shí)候我們可以將頁(yè)面放在resources/templates目錄下,名稱為:login.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>一個(gè)更丑的登錄頁(yè)面</title>
</head>
<body>
<form method="post" action="/login">
用戶名:<input name="username" placeholder="請(qǐng)輸入用戶名" type="text">
密碼:<input name="password" placeholder="請(qǐng)輸入密碼" type="password">
<input value="登錄" type="submit">
</form>
</body>
</html>
3. 將SpringSecurity指向自定義的登錄頁(yè)面
(1)調(diào)整WebSecurityConfig注入的WebSecurityConfigurerAdapter,在and().formLogin()后面增加loginPage("/login")以指定登錄頁(yè)面的uri地址,同時(shí)關(guān)閉csrf安全保護(hù)。
@Bean
public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() {
return new WebSecurityConfigurerAdapter() {
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.
authorizeRequests().antMatchers("/guest/**").permitAll().
...省略部分代碼...
and().formLogin().loginPage("/login").
and().authorizeRequests().anyRequest().permitAll().
and().csrf().disable();
}
};
}
(2)TestController增加login方法(注意我們之前在TestController類上注解了@RestController,這里要記得改成@Controller,否則訪問(wèn)/login的時(shí)候會(huì)直接返回字符串而不是返回html頁(yè)面。另外除了下面新增的/login方法其他方法要增加注解@ResponseBody)
@GetMapping("login")
public String login() {
return "login";
}
4. 測(cè)試及其他
測(cè)試過(guò)程就略吧。還有一些要囑咐的東西給小白們:
- 我們通過(guò)loginPage("/login")來(lái)告知SpringSecurity自定義登錄頁(yè)面的uri路徑,同時(shí)這個(gè)設(shè)定也告知了用戶點(diǎn)擊登錄按鈕的時(shí)候form表單post的uri路徑。即:如果SpringSecurity判定需要用戶登錄,會(huì)將302到/login (get請(qǐng)求),用戶輸入用戶名和密碼點(diǎn)擊登錄按鈕后,也需要我們自定義頁(yè)面post到/login才能讓SpringSecurity完成用戶認(rèn)證過(guò)程。
- 關(guān)于html中輸入用戶名的input的name屬性值本例為username、輸入密碼的input的name屬性值本例為password,這是因?yàn)镾pringSecurity在接收用戶登錄請(qǐng)求時(shí)候默認(rèn)的參數(shù)名就是username和password、如果想更改這兩個(gè)參數(shù)名,可以這樣設(shè)定:and().formLogin().loginPage("/login").usernameParameter("username").passwordParameter("password")
- 測(cè)試過(guò)程中我們可以試著輸錯(cuò)用戶名和密碼點(diǎn)擊登錄,會(huì)發(fā)現(xiàn)頁(yè)面又重新跳轉(zhuǎn)到 http://127.0.0.1:8080/login?error ,只不過(guò)后面增加了參數(shù)error且沒(méi)有參數(shù)值。所以需要我們?cè)賚ogin.html中處理相應(yīng)的邏輯。當(dāng)然你也可以指定用戶認(rèn)證失敗時(shí)候的跳轉(zhuǎn)地址,可以這樣設(shè)定:and().formLogin().loginPage("/login").failureForwardUrl("/login/error")
- 測(cè)試過(guò)程中,如果我們直接訪問(wèn)http://127.0.0.1:8080/login,輸入正確的用戶名和密碼后跳轉(zhuǎn)到http://127.0.0.1:8080即網(wǎng)站根目錄。如果你想指定用戶登錄成功后的默認(rèn)跳轉(zhuǎn)地址,可以這樣設(shè)定:and().formLogin().loginPage("/login").successForwardUrl("/login/success")
七、登出
登出呢?有登錄了,怎么能沒(méi)有登出呢?其實(shí)SpringSecurity已經(jīng)早早的為我們默認(rèn)了一個(gè)登出功能,你訪問(wèn):http://127.0.0.1:8080/logout 試試看?
如果想做我們自己的個(gè)性化登出,可以繼續(xù)調(diào)整WebSecurityConfig注入的WebSecurityConfigurerAdapter
@Bean
public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() {
return new WebSecurityConfigurerAdapter() {
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.
authorizeRequests().antMatchers("/guest/**").permitAll().
and().authorizeRequests().antMatchers("/admin/**").hasRole("admin").
and().authorizeRequests().antMatchers("/authenticated/**").authenticated().
and().authorizeRequests().antMatchers("/permission1/**").hasAuthority("permission1").
and().authorizeRequests().antMatchers("/permission2/**").hasAuthority("permission2").
and().authorizeRequests().antMatchers("/permission3/**").hasAuthority("permission3").
and().authorizeRequests().antMatchers("/permission4/**").hasAuthority("permission4").
and().formLogin().loginPage("/login").
and().logout().logoutUrl("/logout").logoutSuccessUrl("/logoutSuccess").
invalidateHttpSession(true).deleteCookies("cookiename").
addLogoutHandler(new MyLogoutHandle()).logoutSuccessHandler(new MyLogoutSuccessHandle()).
and().authorizeRequests().anyRequest().permitAll().
and().csrf().disable();
}
};
}
MyLogoutHandle實(shí)現(xiàn)了LogoutHandler接口:
public class MyLogoutHandle implements LogoutHandler {
@Override
public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
System.out.println("==================>>>> LogoutHandler Begin");
System.out.println(authentication.getPrincipal());
System.out.println("==================>>>> LogoutHandler End");
}
}
•MyLogoutSuccessHandle實(shí)現(xiàn)了LogoutSuccessHandler接口:
public class MyLogoutSuccessHandle implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("==================>>>> LogoutSuccessHandler Begin");
System.out.println(authentication.getPrincipal());
System.out.println("==================>>>> LogoutSuccessHandler End");
}
}
- logoutUrl():告訴SpringSecurity用戶登出的接口uri地址是什么
- logoutSuccessUrl():告訴SpringSecurity完成用戶登出后要跳轉(zhuǎn)到哪個(gè)地址。如果設(shè)定了LogoutSuccessHandler則logoutSuccessUrl設(shè)定無(wú)效
- invalidateHttpSession:執(zhí)行登出的同時(shí)是否清空session
- deleteCookies:執(zhí)行登出的同時(shí)刪除那些cookie
- addLogoutHandler:執(zhí)行登出的同時(shí)執(zhí)行那些代碼
八、SpringSecurity在Restfull中的變通使用
當(dāng)前環(huán)境前后盾分離已經(jīng)是大趨勢(shì)了吧,除非那些很小很小的項(xiàng)目。所以SpringBoot項(xiàng)目更多的時(shí)候?yàn)榍岸颂峁┙涌?,而并不提供前端?yè)面路由的功能。所以,當(dāng)SpringSecurity在Restfull開(kāi)發(fā)中還需要變通一下:
1.首先我們通過(guò)and().formLogin().loginPage("/login")設(shè)定的跳轉(zhuǎn)到登錄頁(yè)面的GET請(qǐng)求不再指向html,而是直接返回json數(shù)據(jù)告知前端需要用戶登錄。
2.用戶執(zhí)行登錄的時(shí)候,前端執(zhí)行post請(qǐng)求到/login進(jìn)行用戶身份校驗(yàn)。
3.然后我們通過(guò)and().formLogin().failureForwardUrl("/login/error")和and().formLogin().successForwardUrl("/login/error")設(shè)定的登錄成功和失敗跳轉(zhuǎn)來(lái)地址來(lái)返回json數(shù)據(jù)給前端告知其用戶認(rèn)證結(jié)果。
4.最后我們通過(guò)and().logout().logoutSuccessHandler(new MyLogoutSuccessHandle())來(lái)返回json數(shù)據(jù)給前端告知用戶已經(jīng)完成登出。
九、SpringSecurity+SpringSession+Redis
接下來(lái)還有一個(gè)問(wèn)題要處理。在上面的案例中,session都是存儲(chǔ)在servlet容器中的,如果我們需要多點(diǎn)部署負(fù)載均衡的話,就會(huì)出現(xiàn)問(wèn)題。比如:我們部署了兩個(gè)服務(wù)并做了負(fù)載均衡,用戶登錄時(shí)調(diào)用其中一臺(tái)服務(wù)進(jìn)行身份認(rèn)證通過(guò)并將用戶登錄信息存儲(chǔ)在了這臺(tái)服務(wù)器的session里,接下來(lái)用戶訪問(wèn)其他接口,由于負(fù)載均衡的存在用戶請(qǐng)求被分配到了另一個(gè)服務(wù)上,該服務(wù)檢測(cè)用戶session不存在啊,于是就拒絕訪問(wèn)。
在SpringBoot環(huán)境下解決這個(gè)問(wèn)題也很簡(jiǎn)答,很容易就想到SpringSession。所以我們嘗試用SpringSession+Redis解決此問(wèn)題
1. 增加pom依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
2. 修改application.yml
spring: redis: host: 192.168.2.12 port: 6379 password: 123456 session: store-type: redis
3. 修改主啟動(dòng)類,增加@EnableRedisHttpSession注解,開(kāi)啟SpringSession
十、通過(guò)注解的方式實(shí)現(xiàn)權(quán)限控制
首先要在主啟動(dòng)類上增加@EnableGlobalMethodSecurity注解,具體參數(shù)如下:
1.@EnableGlobalMethodSecurity(securedEnabled=true)
支持@Secured注解,例如
@Secured("ROLE_adminRole")
2.@EnableGlobalMethodSecurity(jsr250Enabled=true)
支持@RolesAllowed、@DenyAll、@PermitAll 注解,例如:
@RolesAllowed("ROLE_guestRole")
3.@EnableGlobalMethodSecurity(prePostEnabled=true)
支持@PreAuthorize、@PostAuthorize、@PreFilter、@PostFilter注解,它們使用SpEL能夠在方法調(diào)用上實(shí)現(xiàn)更有意思的安全性約束
- @PreAuthorize :在方法調(diào)用之前,基于表達(dá)式的計(jì)算結(jié)果來(lái)限制對(duì)方法的訪問(wèn),只有表達(dá)式計(jì)算結(jié)果為true才允許執(zhí)行方法
- @PostAuthorize 在方法調(diào)用之后,允許方法調(diào)用,但是如果表達(dá)式計(jì)算結(jié)果為false,將拋出一個(gè)安全性異常
- @PostFilter 允許方法調(diào)用,但必須按照表達(dá)式來(lái)過(guò)濾方法的結(jié)果
- @PreFilter 允許方法調(diào)用,但必須在進(jìn)入方法之前過(guò)濾輸入值
由于這里涉及到SpEL表達(dá)式,所以本文就不詳細(xì)說(shuō)了。
十一、在Controller中獲取當(dāng)前登錄用戶
public String getAuthenticatedData(HttpSession session) {
//SecurityContext securityContext = SecurityContextHolder.getContext();
SecurityContext securityContext = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT");
// 以上獲取securityContext的兩種方法二選一
WebAuthenticationDetails userDetailsService = (WebAuthenticationDetails) securityContext.getAuthentication().getDetails();
UserDetails userDetails = (UserDetails) securityContext.getAuthentication().getPrincipal();
System.out.println("===userDetailsService.getRemoteAddress()===>>" + userDetailsService.getRemoteAddress());
System.out.println("===userDetailsService.getSessionId()===>>" + userDetailsService.getSessionId());
System.out.println("===userDetails.getRemoteAddress()===>>" + userDetails.getUsername());
System.out.println("===userDetails.getPassword()===>>" + userDetails.getPassword());
System.out.println("===userDetails.getAuthorities()===>>" + userDetails.getAuthorities());
return "authenticatedData";
}
十二、總結(jié)
SpringSecurity的使用基本就上面這些。就業(yè)務(wù)邏輯來(lái)說(shuō),SpringSecurity中所謂的role概念嚴(yán)格意義并不能稱之為“角色”。理由是:如果我們的權(quán)限控制比較簡(jiǎn)單,整個(gè)系統(tǒng)中的角色以及角色所擁有的權(quán)限是固定的,那么我們可以將SpringSecurity的role概念拿來(lái)即用。但是如果我們的權(quán)限控制是可配置,用戶和角色是多對(duì)多關(guān)系、角色和權(quán)限也是多對(duì)多關(guān)系,那么我們只能講SpringSecurity的role當(dāng)做“權(quán)限”來(lái)使用。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java數(shù)據(jù)結(jié)構(gòu)之樹(shù)和二叉樹(shù)的相關(guān)資料
這篇文章主要介紹了Java?數(shù)據(jù)結(jié)構(gòu)之樹(shù)和二叉樹(shù)相關(guān)資料,文中通過(guò)示例代碼和一些相關(guān)題目來(lái)做介紹,非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下!2023-01-01
關(guān)于SpringSecurity配置403權(quán)限訪問(wèn)頁(yè)面的完整代碼
本文給大家分享SpringSecurity配置403權(quán)限訪問(wèn)頁(yè)面的完整代碼,配置之前和配置之后的詳細(xì)介紹,代碼簡(jiǎn)單易懂,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2021-06-06
作為程序員必須掌握的Java虛擬機(jī)中的22個(gè)重難點(diǎn)(推薦0
這篇文章主要介紹了Java虛擬機(jī)中22個(gè)重難點(diǎn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
解決spring 處理request.getInputStream()輸入流只能讀取一次問(wèn)題
這篇文章主要介紹了解決spring 處理request.getInputStream()輸入流只能讀取一次問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09
SpringBoot開(kāi)發(fā)項(xiàng)目,引入JPA找不到findOne方法的解決
這篇文章主要介紹了SpringBoot開(kāi)發(fā)項(xiàng)目,引入JPA找不到findOne方法的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
基于SpringBoot + Redis實(shí)現(xiàn)密碼暴力破解防護(hù)
在現(xiàn)代應(yīng)用程序中,保護(hù)用戶密碼的安全性是至關(guān)重要的,密碼暴力破解是指通過(guò)嘗試多個(gè)密碼組合來(lái)非法獲取用戶賬戶的密碼,為了保護(hù)用戶密碼不被暴力破解,我們可以使用Spring Boot和Redis來(lái)實(shí)現(xiàn)一些防護(hù)措施,本文將介紹如何利用這些技術(shù)來(lái)防止密碼暴力破解攻擊2023-06-06
基于springboot實(shí)現(xiàn)數(shù)據(jù)可視化的示例代碼
本文主要介紹了基于springboot實(shí)現(xiàn)數(shù)據(jù)可視化,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧<BR>2022-07-07

