SpringBoot安全認證Security

cnnbull發表於2021-09-09

一、基本環境搭建

父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. 建立測試用Controller

@RestController
public class TestController {

    @GetMapping("getData")
    public String getData() {
        return "date";
    }

}

3. 建立SpringBoot啟動類並run

@SpringBootApplication
public class SpringBootTestApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootTestApplication.class, args);
    }

}

4. 測試

訪問,由於我們開啟了SpringSecurity且當前是未登入狀態,頁面會被302重定向到,頁面如下:
圖片描述
使用者名稱:user,密碼可以在控制檯輸出中找到:
圖片描述
輸入正確的使用者名稱和密碼後點選Login按鈕即被重新302到並顯示查詢資料:
圖片描述
這表示我們的介面已經被spring保護了。
那麼肯定會有小夥伴吐槽了,這麼複雜的密碼,鬼才記得住,所以…

二、為Spring Security設定使用者名稱和密碼

為了解決複雜密碼的問題,我們可以在application.yml中做如下設定:

spring:
  security:
    user:
      name: user
      password: 123

這樣我們就可以透過使用者名稱user密碼123來訪問介面了。
然後肯定又有小夥伴吐槽了,整個系統就一個使用者麼?有哪個系統是隻有一個使用者的?所以…

三、為Spring Security設定多個使用者

如果想要給Spring Security設定多個使用者可用,則新建一個class,實現介面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;
    }

}
  1. 注意需要註解@EnableWebSecurity
  2. InMemoryUserDetailsManager:顧名思義,將使用者名稱密碼儲存在記憶體中的使用者管理器。我們透過這個管理器增加了兩個使用者,分別是:使用者名稱admin密碼admin,使用者名稱guest密碼guest。

做完如上更改後重啟應用,再次訪問,輸入admin/admin或guest/guest即可透過身份驗證並正常使用介面了。
看到這肯定又有小夥伴要吐槽了:使用者資料直接硬編碼到程式碼裡是什麼鬼!我要把使用者放在資料庫!所以…

四、SpringSecurity+Mysql

想要使用資料庫,那麼我們可以

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. 配置資料庫連線

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. 建立測試用表結構及資料

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');

我們建立了使用者資訊表,並插入兩個使用者資訊,使用者名稱/密碼依然是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. 調整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();
    };
}

做完如上更改後重啟應用,再次訪問,輸入admin/admin或guest/guest即可透過身份驗證並正常使用介面了。
關於UserDetailsService,有些東西要說明下:

PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String passwordAfterEncoder = passwordEncoder.encode(password);

上面這兩句程式碼是在對使用者密碼進行加密。為什麼要這樣子呢?看到這肯定又有小夥伴會吐槽:資料庫儲存銘文密碼是什麼鬼!對,Spring也是儘量在幫助開發者避免這個事情。所以SpringSecurity在進行密碼比對的時候需要開發者提供加密後的密碼。我們上面的寫法其實是不合理的,實際情況應該是資料庫中儲存密文密碼,然後將資料庫中的密碼直接傳給User.password()就可以了。

6. 關於SpringSecurity加密後的密文格式

我們可以透過打斷點的方式或者增加

System.out.println(username + "---->>>" + passwordAfterEncoder);

來檢視下,如果admin/admin被登入時候,passwordAfterEncoder的值是什麼?輸出結果:

admin---->>>{bcrypt}$2a$10$d4VkiIfP7MyNSipjLtQ0Keva4ST6U6Fnw77iiv39IGnGswptqWRG.
guest---->>>{bcrypt}$2a$10$8jRMbiGzFIS4GU3SWAm83eWgFO29EEb5QhXOEkPEaabw5Oiy/jxUC

可以看出加密後的密碼可以分為兩部分

  • {}內描述了加密演算法,這裡為bcrypt演算法。
  • {}後面即為密文密碼,這裡是包含鹽的。

所以SpringSecurity的工作原理就是:當使用者輸入使用者名稱和密碼點選Login以後,SpringSecurity先透過呼叫我們自定義的UserDetailsService獲取到加密後密碼,然後根據{}裡的內容獲知加密演算法,再將使用者輸入的密碼按照該演算法進行加密,最後再與{}後的密文密碼比對即可獲知使用者憑據是否有效。
透過檢視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已經不建議使用了。SpringSecurity認為:

Digest based password encoding is not considered secure.  //基於摘要的密碼編碼被認為是不安全的

五 、許可權控制

以上內容我們只解決了使用者登入問題,但是實際開發中僅僅完成使用者登入是不夠的,我們還需要使用者授權及授權驗證。由於我們已經將使用者資訊儲存到資料庫裡了,那麼姑且我們也將許可權資訊儲存在資料庫吧。

1. 準備資料庫表及測試資料

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);
  1. role:角色資訊表,permission許可權資訊表,user_r_role使用者所屬角色表,role_r_permission角色擁有許可權表,user_r_permission使用者擁有許可權表。
  2. 由於使用者有所屬角色且角色是有許可權的,使用者同時又單獨擁有許可權,所以使用者最終擁有的許可權取並集。
  3. 使用者admin最終擁有角色adminRole以及許可權:permission1、permission2、permission3、permission4
  4. 使用者guest最終擁有角色guestRole以及許可權:permission3、permission4

2. Dao、Service

dao增加方法:根據使用者名稱查角色以及根據使用者名稱查許可權

public List<String> getPermissionsByUsername(String username) {
    String sql =
            "select d.permissionn" +
            "from user an" +
            "       join user_r_role b on a.id = b.useridn" +
            "       join role_r_permission c on b.roleid = c.roleidn" +
            "       join permission d on c.permissionid = d.idn" +
            "where a.username = ?n" +
            "unionn" +
            "select c.permissionn" +
            "from user an" +
            "       join user_r_permission b on a.id = b.useridn" +
            "       join permission c on b.permissionid = c.idn" +
            "where a.username = ?";
    return jdbcTemplate.queryForList(sql, new String[]{username, username}, String.class);
}

public List<String> getRoleByUsername(String username) {
    String sql =
            "select c.rolen" +
            "from user an" +
            "       join user_r_role b on a.id = b.useridn" +
            "       join role c on b.roleid = c.idn" +
            "where a.username = ?";
    return jdbcTemplate.queryForList(sql, new String[]{username}, String.class);
}

service增加方法:根據使用者名稱查角色以及根據使用者名稱查許可權

public List<String> getPermissionsByUsername(String username) {
    return loginDao.getPermissionsByUsername(username);
}

public List<String> getRoleByUsername(String username) {
    return loginDao.getRoleByUsername(username);
}

3. WebSecurityConfig

(1)調整public UserDetailsService userDetailsService()方法,在構建使用者資訊的時候把使用者所屬角色和使用者所擁有的許可權也填充上(最後return的時候)。

@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();
    };
}

這裡面有個坑,就是紅色程式碼部分。具體可檢視org.springframework.security.core.userdetails.User.UserBuilder。roles()方法和authorities()方法實際上都是在針對UserBuilder的authorities屬性進行set操作,執行roles(“roleName”)和執行authorities(“ROLE_roleName”)是等價的。所以上例程式碼中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,為我們需要的保護的介面設定需要許可權驗證:

@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();
        }
    };
}
  1. /guest/**的介面會被允許所有人訪問,包括未登入的人。
  2. /admin/**的介面只能被擁有admin角色的使用者訪問。
  3. /authenticated/**的介面可以被所有已經登入的使用者訪問。
  4. /permission1/的介面可以被擁有permission1許可權的使用者訪問。/permission2/、/permission3/**、/permission4/**同理

4. TestController

最後我們調整下TestContrller,增加幾個介面以便測試:

@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. 測試

  • 訪問/guest/getData無需登入即可訪問成功。
  • 訪問/authenticated/getData,會彈出使用者登入頁面。登入任何一個使用者都可訪問成功。
  • 訪問/admin/getData,會彈出使用者登入頁面。登入admin使用者訪問成功,登入guest使用者會發生錯誤,403未授權。
  • 其他的就不再贅述了。

六、自定義登入頁面

是不是覺得SpringScurity的登入頁面醜爆了?是不是想老子還能做一個更醜的登入頁面你信不信?接下來我們來弄一個更醜的登入頁面。

1. 增加pom依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

2. 編寫自己的登入頁面

thymeleaf預設的頁面放置位置為:classpath:templates/ 目錄下,所以在編寫程式碼的時候我們可以將頁面放在resources/templates目錄下,名稱為:login.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>一個更醜的登入頁面</title>
</head>
<body>
    <form method="post" action="/login">
        使用者名稱:<input name="username" placeholder="請輸入使用者名稱" type="text">
        密碼:<input name="password" placeholder="請輸入密碼" type="password">
        <input value="登入" type="submit">
    </form>
</body>
</html>

3. 將SpringSecurity指向自定義的登入頁面

(1)調整WebSecurityConfig注入的WebSecurityConfigurerAdapter,在and().formLogin()後面增加loginPage("/login")以指定登入頁面的uri地址,同時關閉csrf安全保護。

@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,否則訪問/login的時候會直接返回字串而不是返回html頁面。另外除了下面新增的/login方法其他方法要增加註解@ResponseBody)

@GetMapping("login")
public String login() {
    return "login";
}

4. 測試及其他

測試過程就略吧。還有一些要囑咐的東西給小白們:

  • 我們透過loginPage("/login")來告知SpringSecurity自定義登入頁面的uri路徑,同時這個設定也告知了使用者點選登入按鈕的時候form表單post的uri路徑。即:如果SpringSecurity判定需要使用者登入,會將302到/login (get請求),使用者輸入使用者名稱和密碼點選登入按鈕後,也需要我們自定義頁面post到/login才能讓SpringSecurity完成使用者認證過程。
  • 關於html中輸入使用者名稱的input的name屬性值本例為username、輸入密碼的input的name屬性值本例為password,這是因為SpringSecurity在接收使用者登入請求時候預設的引數名就是username和password、如果想更改這兩個引數名,可以這樣設定:and().formLogin().loginPage("/login").usernameParameter(“username”).passwordParameter(“password”)
  • 測試過程中我們可以試著輸錯使用者名稱和密碼點選登入,會發現頁面又重新跳轉到 ,只不過後面增加了引數error且沒有引數值。所以需要我們再login.html中處理相應的邏輯。當然你也可以指定使用者認證失敗時候的跳轉地址,可以這樣設定:and().formLogin().loginPage("/login").failureForwardUrl("/login/error")
  • 測試過程中,如果我們直接訪問,輸入正確的使用者名稱和密碼後跳轉到即網站根目錄。如果你想指定使用者登入成功後的預設跳轉地址,可以這樣設定:and().formLogin().loginPage("/login").successForwardUrl("/login/success")

七、登出

登出呢?有登入了,怎麼能沒有登出呢?其實SpringSecurity已經早早的為我們預設了一個登出功能,你訪問: 試試看?
如果想做我們自己的個性化登出,可以繼續調整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實現了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實現了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完成使用者登出後要跳轉到哪個地址。如果設定了LogoutSuccessHandler則logoutSuccessUrl設定無效
  • invalidateHttpSession:執行登出的同時是否清空session
  • deleteCookies:執行登出的同時刪除那些cookie
  • addLogoutHandler:執行登出的同時執行那些程式碼

八、SpringSecurity在Restfull中的變通使用

當前環境前後盾分離已經是大趨勢了吧,除非那些很小很小的專案。所以SpringBoot專案更多的時候為前端提供介面,而並不提供前端頁面路由的功能。所以,當SpringSecurity在Restfull開發中還需要變通一下:

  1. 首先我們透過and().formLogin().loginPage("/login")設定的跳轉到登入頁面的GET請求不再指向html,而是直接返回json資料告知前端需要使用者登入。
  2. 使用者執行登入的時候,前端執行post請求到/login進行使用者身份校驗。
  3. 然後我們透過and().formLogin().failureForwardUrl("/login/error")和and().formLogin().successForwardUrl("/login/error")設定的登入成功和失敗跳轉來地址來返回json資料給前端告知其使用者認證結果。
  4. 最後我們透過and().logout().logoutSuccessHandler(new MyLogoutSuccessHandle())來返回json資料給前端告知使用者已經完成登出。

九、SpringSecurity+SpringSession+Redis

接下來還有一個問題要處理。在上面的案例中,session都是儲存在servlet容器中的,如果我們需要多點部署負載均衡的話,就會出現問題。比如:我們部署了兩個服務並做了負載均衡,使用者登入時呼叫其中一臺服務進行身份認證透過並將使用者登入資訊儲存在了這臺伺服器的session裡,接下來使用者訪問其他介面,由於負載均衡的存在使用者請求被分配到了另一個服務上,該服務檢測使用者session不存在啊,於是就拒絕訪問。
在SpringBoot環境下解決這個問題也很簡答,很容易就想到SpringSession。所以我們嘗試用SpringSession+Redis解決此問題

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

十、透過註解的方式實現許可權控制

首先要在主啟動類上增加@EnableGlobalMethodSecurity註解,具體引數如下:

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能夠在方法呼叫上實現更有意思的安全性約束

  • @PreAuthorize :在方法呼叫之前,基於表示式的計算結果來限制對方法的訪問,只有表示式計算結果為true才允許執行方法
  • @PostAuthorize 在方法呼叫之後,允許方法呼叫,但是如果表示式計算結果為false,將丟擲一個安全性異常
  • @PostFilter 允許方法呼叫,但必須按照表示式來過濾方法的結果
  • @PreFilter 允許方法呼叫,但必須在進入方法之前過濾輸入值

由於這裡涉及到SpEL表示式,所以本文就不詳細說了。

十一、在Controller中獲取當前登入使用者

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";
    }

十二、總結

SpringSecurity的使用基本就上面這些。就業務邏輯來說,SpringSecurity中所謂的role概念嚴格意義並不能稱之為“角色”。理由是:如果我們的許可權控制比較簡單,整個系統中的角色以及角色所擁有的許可權是固定的,那麼我們可以將SpringSecurity的role概念拿來即用。但是如果我們的許可權控制是可配置,使用者和角色是多對多關係、角色和許可權也是多對多關係,那麼我們只能講SpringSecurity的role當做“許可權”來使用。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2524/viewspace-2822975/,如需轉載,請註明出處,否則將追究法律責任。

相關文章