Spring Security安全管理
目前主流的安全管理框架主要有Spring Security和Shiro畜侦。Shiro是一個(gè)輕量級(jí)框架,配置較為簡(jiǎn)單澎语。而Spring Security則較為復(fù)雜擅羞,但功能相對(duì)較多减俏。
Spring Boot 中對(duì)Spring Security做了一系列自動(dòng)化配置垄懂,使得在Spring Boot中使用Spring Security相當(dāng)方便草慧。
Spring Security
當(dāng)引入Spring Security依賴后漫谷,所有的接口都將被保護(hù)起來(lái)舔示,訪問(wèn)接口時(shí)需要輸入用戶名和密碼竖共。用戶名默認(rèn)為user俺祠,密碼在控制臺(tái)隨機(jī)生成蜘渣。這是spring boot 為spring security提供的自動(dòng)化配置蔫缸。
當(dāng)然拾碌,登錄的用戶名可以自己配置倦沧,配置的方法主要有兩種
- 在配置文件中配置
在application.properties中添加如下配置展融,即可設(shè)置登錄的用戶名和密碼
spring.security.user.name=admin
spring.security.user.password=123
spring.security.user.roles=admin
- 使用Java代碼配置
創(chuàng)建Security配置類告希,繼承自WebSecurityConfigurerAdapter類燕偶,重寫
configure(AuthenticationManagerBuilder auth)方法酝惧,如下:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("123").roles("admin")
.and()
.withUser("zby").password("123").roles("user");
}
這里設(shè)置了兩個(gè)用戶admin和zby晚唇,用戶角色分別是admin和user哩陕。
HttpSecurity
HttpSecurity是Spring Security Config用于配置http請(qǐng)求安全控制的安全構(gòu)建器(類似于Spring Security XML配置中的http命名空間配置部分)悍及,它的構(gòu)建目標(biāo)是一個(gè)SecurityFilterChain,實(shí)現(xiàn)類使用DefaultSecurityFilterChain心赶。該目標(biāo)SecurityFilterChain最終會(huì)被Spring Security的安全過(guò)濾器FilterChainProxy所持有和應(yīng)用于相應(yīng)的http請(qǐng)求的安全控制园担。
spring security類中為我們提供了configure(HttpSecurity http)弯汰,可以在這個(gè)方法中配置攔截規(guī)則咏闪,實(shí)現(xiàn)http請(qǐng)求的安全管理
使用方法如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //開啟配置
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("user/**").hasAnyRole("admin","user")
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/doLogin")
.permitAll()
.and()
.csrf().disable();
}
控制器
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
@GetMapping("/admin/hello")
public String admin(){
return "hello admin";
}
@GetMapping("/user/hello")
public String user(){
return "hello user";
}
}
這里設(shè)置了admin、user角色的訪問(wèn)權(quán)限据某,/admin的接口僅允許角色為admin的用戶訪問(wèn)癣籽,/user接口角色為user和admin用戶都可訪問(wèn)筷狼,其他頁(yè)面登錄后即可訪問(wèn)埂材。
登錄zby用戶后俏险,若要訪問(wèn)/admin/hello接口竖独,瀏覽將會(huì)報(bào)錯(cuò)预鬓,顯示沒(méi)有權(quán)限
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Tue Feb 25 15:11:50 CST 2020
There was an unexpected error (type=Forbidden, status=403).
Forbidden
/hello和/user/hello則可以正常訪問(wèn)格二。
配置多個(gè)HttpSecurity
@Configuration
@Order(1)
public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/admin/**").authorizeRequests().anyRequest().hasRole("admin");
}
}
@Configuration
public static class OtherSecurity extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll()
.and()
.csrf().disable();
}
}
表單登錄配置
配置表單登錄時(shí)我們可以在 successHandler方法中沧奴,配置登錄成功的回調(diào)滔吠,如果是前后端分離開發(fā)的話疮绷,登錄成功后返回 JSON 即可冬骚,同理,failureHandler 方法中配置登錄失敗的回調(diào),logoutSuccessHandler 中則配置注銷成功的回調(diào)舍悯。
//登錄成功的處理器
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("status",200);
map.put("msg",authentication.getPrincipal());
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
})
登錄成功后返回的json
{
"msg": {
"password": null,
"username": "admin",
"authorities": [
{
"authority": "ROLE_admin"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"status": 200
}
登錄失敗的處理器
//登錄失敗的處理器
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("status",401);
if(e instanceof LockedException){
map.put("msg","賬號(hào)被鎖定,登錄失敗");
}else if(e instanceof BadCredentialsException){
map.put("msg","用戶名或密碼錯(cuò)誤,登錄失敗");
}
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
})
注銷登錄
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("status",200);
map.put("msg","注銷成功");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
})
注銷成功,返回的json
{
"msg": "注銷成功",
"status": 200
}
密碼加密
在Spring5之后奄薇,密碼必須加密后才能應(yīng)用馁蒂。加密密碼則需要配置一個(gè)密碼的編碼器,可以通過(guò)PasswordEncoder實(shí)現(xiàn)
spring security中提供了BCryptPasswordEncoder工具進(jìn)行密碼加密饵隙,如將同一串進(jìn)行十次加密
@Test
void contextLoads() {
for (int i = 0; i < 10; i++) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println(encoder.encode("123"));
}
}
得到結(jié)果如下:
同一串字符每次加密產(chǎn)生的結(jié)構(gòu)不同金矛,這就實(shí)現(xiàn)了密碼的加密驶俊。
方法安全
Spring Security框架支持通過(guò)在方法上加注解來(lái)確保方法的安全饼酿。
方法安全在Spring Security中默認(rèn)是沒(méi)有開啟的故俐,在Spring Security配置類上加@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true),開啟方法安全的相關(guān)注解
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class MultiHttpSecurityConfig{
......
}
創(chuàng)建Service類
@Service
public class MethodService {
@PreAuthorize("hasRole('admin')")
public String admin(){
return "hello admin";
}
@Secured("ROLE_user")
public String user(){
return "hello user";
}
@PreAuthorize("hasAnyRole('admin','author')")
public String hello(){
return "hello world";
}
}
為三個(gè)方法分別賦予相應(yīng)的角色。
在Controller中調(diào)用三個(gè)方法
@Autowired
MethodService methodService;
@GetMapping("/hello1")
public String hello1(){
return methodService.admin();
}
@GetMapping("/hello2")
public String hello2(){
return methodService.user();
}
@GetMapping("/hello3")
public String hello3(){
return methodService.hello();
}
這時(shí)每個(gè)接口都可以被訪問(wèn)刚陡,但只有相應(yīng)的角色才能調(diào)用接口中的方法。
基于數(shù)據(jù)庫(kù)的認(rèn)證
創(chuàng)建項(xiàng)目后并配置數(shù)據(jù)庫(kù)信息
spring.datasource.url=jdbc:mysql://localhost:3306/demo?serverTimezone=GMT%2B8&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=admin
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
創(chuàng)建User和Role的實(shí)體類
@Data
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean locked;
private List<Role> roles;
//返回用戶所以角色
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
//賬戶是否未過(guò)期
@Override
public boolean isAccountNonExpired() {
return true;
}
//賬戶是否未鎖定
@Override
public boolean isAccountNonLocked() {
return locked;
}
//密碼是否未過(guò)期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//是否可用
@Override
public boolean isEnabled() {
return enabled;
}
}
這里定義用戶時(shí)需要實(shí)現(xiàn)UserDetails接口。
@Data
public class Role {
private Integer id;
private String name;
private String nameZh;
}
Service:
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if(user == null){
throw new UsernameNotFoundException("用戶名不存在");
}
user.setRoles(userMapper.getUserRolesById(user.getId()));
return user;
}
}
配置SpringSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
角色繼承的配置
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_dba > ROLE_admin \n ROLE_admin > ROLE_user";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
配置完成后,接下來(lái)指定角色和資源的對(duì)應(yīng)關(guān)系即可股淡,如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/admin/**")
.hasRole("admin")
.antMatchers("/db/**")
.hasRole("dba")
.antMatchers("/user/**")
.hasRole("user")
.and()
.formLogin()
.loginProcessingUrl("/doLogin")
.permitAll()
.and()
.csrf().disable();
}
這個(gè)表示 /db/** 格式的路徑需要具備 dba 角色才能訪問(wèn), /admin/** 格式的路徑則需要具備 admin 角色才能訪問(wèn)埠帕, /user/** 格式的路徑敛瓷,則需要具備 user 角色才能訪問(wèn)呐籽,此時(shí)提供相關(guān)接口狡蝶,會(huì)發(fā)現(xiàn)牢酵,dba 除了訪問(wèn) /db/** 馍乙,也能訪問(wèn) /admin/** 和 /user/** 撑瞧,admin 角色除了訪問(wèn) /admin/** 预伺,也能訪問(wèn) /user/** 酬诀,user 角色則只能訪問(wèn) /user/** 瞒御。
動(dòng)態(tài)權(quán)限配置
動(dòng)態(tài)權(quán)限配置就是要將權(quán)限也存入數(shù)據(jù)庫(kù)中肴裙,通過(guò)數(shù)據(jù)庫(kù)中數(shù)據(jù)之間的關(guān)系來(lái)確定權(quán)限蜻懦。
數(shù)據(jù)庫(kù)權(quán)限如下圖所示
[圖片上傳失敗...(image-a9aca8-1582887592116)]
通過(guò)user確定role宛乃,在通過(guò)role定位到相應(yīng)的menu
要實(shí)現(xiàn)動(dòng)態(tài)權(quán)限配置烤惊,首先要配置過(guò)濾器渡贾,創(chuàng)建一個(gè)filter類空骚,實(shí)現(xiàn)FilterInvocationSecurityMetadataSource接口囤屹,并實(shí)現(xiàn)接口中的方法
@Component
public class MyFilter implements FilterInvocationSecurityMetadataSource {
//路徑匹配符
AntPathMatcher pathMatcher = new AntPathMatcher();
@Autowired
MenuService menuService;
//根據(jù)請(qǐng)求地址肋坚,分析請(qǐng)求需要的角色
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) o).getRequestUrl();
List<Menu> allMenus = menuService.getAllMenus();
for (Menu menu : allMenus) {
if(pathMatcher.match(menu.getPattern(),requestUrl)){
List<Role> roles = menu.getRoles();
String[] rolesStr = new String[roles.size()];
for (int i = 0; i < roles.size(); i++) {
rolesStr[i] = roles.get(i).getName();
}
return SecurityConfig.createList(rolesStr);
}
}
return SecurityConfig.createList("ROLE_login");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
另外還需要配置一個(gè)類用于查詢是否具備請(qǐng)求需要的角色,若不存在則該請(qǐng)求是一個(gè)非法請(qǐng)求铣鹏,該類要實(shí)現(xiàn)AccessDecisionManager接口。該接口提供了三個(gè)方法decide方法和兩個(gè)supports方法合溺,兩個(gè)supports方法默認(rèn)返回值為true辫愉。decide方法中有三個(gè)參數(shù)
void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection)
- authentication:保存當(dāng)前登錄用戶信息,代表用戶擁有的角色
- o:是一個(gè)FilterInvocation對(duì)象依疼,用于獲取當(dāng)前請(qǐng)求對(duì)象,代表需要的角色
- collection:是MyFilter類中Collection<ConfigAttribute> getAttributes(Object o)的返回值
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute attribute : collection) {
if("ROLE_login".equals(attribute.getAttribute())){
//判斷是否登錄误辑,若是匿名用戶則表示沒(méi)有登錄巾钉,拋出異常
if(authentication instanceof AnonymousAuthenticationToken){
throw new AccessDeniedException("非法請(qǐng)求潦匈!");
}else break;
}
//獲取當(dāng)前用戶具備的角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if(authority.getAuthority().equals(attribute.getAttribute())){
break;
}
}
}
throw new AccessDeniedException("非法請(qǐng)求茬缩!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
兩個(gè)類寫完之后凰锡,在security配置類中引入,并在HttpSecurity方法中做如下配置:
@Autowired
MyFilter myFilter;
@Autowired
MyAccessDecisionManager myAccessDecisionManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(myAccessDecisionManager);
o.setSecurityMetadataSource(myFilter);
return o;
}
})
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable();
}
編寫接口測(cè)試
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
@GetMapping("/db/hello")
public String db(){
return "hello db";
}
@GetMapping("/admin/hello")
public String admin(){
return "hello admin";
}
@GetMapping("/user/hello")
public String user(){
return "hello user";
}
}
OAuth2協(xié)議
OAuth(Open Authorization,開放授權(quán))是為用戶資源的授權(quán)定義了一個(gè)安全智绸、開放及簡(jiǎn)單的標(biāo)準(zhǔn)瞧栗,第三方無(wú)需知道用戶的賬號(hào)及密碼,就可獲取到用戶的授權(quán)信息
- 應(yīng)用場(chǎng)景
第三方應(yīng)用授權(quán)登錄:在APP或者網(wǎng)頁(yè)接入一些第三方應(yīng)用時(shí)殴边,時(shí)常會(huì)需要用戶登錄另一個(gè)合作平臺(tái)锤岸,比如QQ是偷,微博蛋铆,微信的授權(quán)登錄,第三方應(yīng)用通過(guò)oauth2方式獲取用戶信息
具體的實(shí)現(xiàn)流程圖如下:
- Spring Security結(jié)合OAuth2
Spring Boot下的OAuth2是在spring security的基礎(chǔ)上完成的留特。
添加OAuth2的依賴:
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.6.RELEASE</version>
</dependency>
將OAuth中的Token令牌放在Redis中,因此需要再添加Redis依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置Redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0
在OAuth2中需要配置兩個(gè)服務(wù)器捧韵,一個(gè)授權(quán)服務(wù)器和一個(gè)資源服務(wù)器
1.配置授權(quán)服務(wù)器
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("password")
.authorizedGrantTypes("password","refresh_token")//配置授權(quán)模式
.accessTokenValiditySeconds(1800)//Token過(guò)期時(shí)間
.resourceIds("rid")
.scopes("all")
.secret("$2a$10$9zMfB82E5BnYvnKriQUdaudC39H5JEu.HN80ywI2EQY/2.MuOj.i.");
}
//配置Token存取
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
//支持clientId和client security做登錄認(rèn)證
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
2.配置資源服務(wù)器
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("rid").stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated();
}
}
根據(jù)OAuth2協(xié)議市咆,先從授權(quán)服務(wù)器中獲取Token,再到資源服務(wù)器上獲取資源再来,判斷給出的Token令牌是否有權(quán)限訪問(wèn)資源蒙兰。
最后配置Security
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
@Bean
protected UserDetailsService userDetailsService() {
return super.userDetailsService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("zby")
.password("$2a$10$9zMfB82E5BnYvnKriQUdaudC39H5JEu.HN80ywI2EQY/2.MuOj.i.")
.roles("admin")
.and()
.withUser("user")
.password("$2a$10$9zMfB82E5BnYvnKriQUdaudC39H5JEu.HN80ywI2EQY/2.MuOj.i.")
.roles("user");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/oauth/**")
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.and().csrf().disable();
}
}
-
測(cè)試
在PostMan中,向測(cè)試接口發(fā)送請(qǐng)求芒篷,可得到token
image
Redis中存儲(chǔ)的Token信息
用Token去訪問(wèn)相應(yīng)資源
token過(guò)期時(shí)可利用refresh_token參數(shù),通過(guò)post請(qǐng)求獲取新的token
得到新的token
{
"access_token": "37a62e16-0774-4fc4-b043-824343b3709b",
"token_type": "bearer",
"refresh_token": "1235097a-d9fd-4342-9c05-a0c2b535b166",
"expires_in": 1799,
"scope": "all"
}
Spring Security使用Json登錄
keyValue形式的登錄主要通過(guò)過(guò)濾器UsernamePasswordAuthenticationFilter來(lái)實(shí)現(xiàn)。所以针炉,要實(shí)現(xiàn)Json登錄需要重新一個(gè)過(guò)濾器挠他。
創(chuàng)建過(guò)濾器MyFilter類,繼承UsernamePasswordAuthenticationFilter并重寫attemptAuthentication方法篡帕。
public class MyFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//先判斷發(fā)來(lái)的是否是Post請(qǐng)求
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
//解析Json
if(request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
//若if條件成立殖侵,說(shuō)明用戶以JSON形式傳遞參數(shù)
String username = null;
String password = null;
try {
Map<String,String> map = new ObjectMapper().readValue(request.getInputStream(), Map.class);
username = map.get("username");
password = map.get("password");
} catch (IOException e) {
e.printStackTrace();
}
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
//否則調(diào)用父類的方法登錄
return super.attemptAuthentication(request, response);
}
}
之后,在Security配置類中進(jìn)行配置镰烧,使MyFilter中的方法生效
整合JWT
JWT拢军,全稱是Json Web Token,是一種JSON風(fēng)格的輕量級(jí)的授權(quán)和身份認(rèn)證規(guī)范怔鳖,可實(shí)現(xiàn)無(wú)狀態(tài)茉唉、分布式的Web應(yīng)用授權(quán)。特別適用于分布式站點(diǎn)的單點(diǎn)登錄(SSO)場(chǎng)景结执。
jwt數(shù)據(jù)格式
jwt數(shù)據(jù)格式一般包括三部分:
1.頭部(Header)
頭部用于描述關(guān)于該JWT的最基本的信息度陆,例如其類型以及簽名所用的算法等。這也可以被表示成一個(gè)JSON對(duì)象献幔。對(duì)頭部進(jìn)行Base64Url編碼(可解碼)坚芜,得到第一部分?jǐn)?shù)據(jù)。
2.載荷(Payload)
就是有效數(shù)據(jù)斜姥,在官方文檔中(RFC7519),這里給了7個(gè)示例信息:
- iss (issuer):表示簽發(fā)人
- exp (expiration time):表示token過(guò)期時(shí)間
- sub (subject):主題
- aud (audience):受眾
- nbf (Not Before):生效時(shí)間
- iat (Issued At):簽發(fā)時(shí)間
- jti (JWT ID):編號(hào)
這部分也會(huì)采用Base64Url編碼,得到第二部分?jǐn)?shù)據(jù)铸敏。
3.簽名(Signature)
是整個(gè)數(shù)據(jù)的認(rèn)證信息缚忧。一般根據(jù)前兩步的數(shù)據(jù),再加上服務(wù)的的密鑰secret(密鑰保存在服務(wù)端杈笔,不能泄露給客戶端)闪水,通過(guò)Header中配置的加密算法生成。用于驗(yàn)證整個(gè)數(shù)據(jù)完整和可靠性蒙具。
將這三部分用.連接成一個(gè)完整的字符串,構(gòu)成了最終的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT交互流程
1.應(yīng)用程序或客戶端向授權(quán)服務(wù)器請(qǐng)求授權(quán)
2.獲取到授權(quán)后球榆,授權(quán)服務(wù)器會(huì)向應(yīng)用程序返回訪問(wèn)令牌
3、應(yīng)用程序使用訪問(wèn)令牌來(lái)訪問(wèn)受保護(hù)資源(如API)
因?yàn)镴WT簽發(fā)的token中已經(jīng)包含了用戶的身份信息禁筏,并且每次請(qǐng)求都會(huì)攜帶持钉,這樣服務(wù)的就無(wú)需保存用戶信息,甚至無(wú)需去數(shù)據(jù)庫(kù)查詢篱昔,這樣就完全符合了RESTful的無(wú)狀態(tài)規(guī)范每强。
在Spring Security中整合JWT
首先創(chuàng)建一個(gè)Spring Boot項(xiàng)目,創(chuàng)建時(shí)添加Spring Security依賴州刽,創(chuàng)建完成后空执,添加 jjwt 依賴,pom.xml文件如下:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
然后在項(xiàng)目中創(chuàng)建一個(gè)簡(jiǎn)單的 User 對(duì)象實(shí)現(xiàn) UserDetails 接口穗椅。
再創(chuàng)建一個(gè)HelloController辨绊,內(nèi)容如下:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello jwt !";
}
@GetMapping("/admin")
public String admin() {
return "hello admin !";
}
}
HelloController有兩個(gè)接口,設(shè)計(jì)是 /hello 接口可以被具有 user 角色的用戶訪問(wèn)匹表,而 /admin 接口則可以被具有 admin 角色的用戶訪問(wèn)门坷。
接下來(lái)提供兩個(gè)和 JWT 相關(guān)的過(guò)濾器配置:
一個(gè)是用戶登錄的過(guò)濾器,在用戶的登錄的過(guò)濾器中校驗(yàn)用戶是否登錄成功桑孩,如果登錄成功拜鹤,則生成一個(gè)token返回給客戶端,登錄失敗則給前端一個(gè)登錄失敗的提示流椒。用戶登錄的過(guò)濾器 JwtLoginFilter 繼承自 AbstractAuthenticationProcessingFilter敏簿,并實(shí)現(xiàn)其中的三個(gè)默認(rèn)方法。
在attemptAuthentication方法中宣虾,從登錄參數(shù)中提取出用戶名密碼惯裕,然后調(diào)用AuthenticationManager.authenticate()方法去進(jìn)行自動(dòng)校驗(yàn)。
如果校驗(yàn)成功绣硝,就會(huì)來(lái)到successfulAuthentication回調(diào)中蜻势,在successfulAuthentication方法中,將用戶角色遍歷然后用一個(gè)“鹉胖,”連接起來(lái)握玛,然后再利用Jwts去生成token够傍,按照代碼的順序,生成過(guò)程一共配置了四個(gè)參數(shù)挠铲,分別是用戶角色冕屯、主題、過(guò)期時(shí)間以及加密算法和密鑰拂苹,然后將生成的token寫出到客戶端安聘。
如果校驗(yàn)失敗就會(huì)來(lái)到unsuccessfulAuthentication方法中,在這個(gè)方法中返回一個(gè)錯(cuò)誤提示給客戶端即可瓢棒。
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException {
User user = new ObjectMapper().readValue(req.getInputStream(),User.class);
return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword()));
}
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException {
Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();//獲取登錄用戶角色
StringBuffer sb = new StringBuffer();
for (GrantedAuthority authority : authorities) {
sb.append(authority.getAuthority()).append(",");
}
String jwt = Jwts.builder()
.claim("authorities", sb)
.setSubject(authResult.getName())
.setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000))
.signWith(SignatureAlgorithm.HS512, "zby@123")
.compact();//生成JWT的Token
Map<String,String> map = new HashMap<>();
map.put("token",jwt);
map.put("msg","登錄成功");
resp.setContentType("application/json:charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException {
Map<String,String> map = new HashMap<>();
map.put("msg","登錄失敗");
resp.setContentType("application/json:charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
}
第二個(gè)過(guò)濾器則是當(dāng)其他請(qǐng)求發(fā)送來(lái)浴韭,校驗(yàn)token的過(guò)濾器,如果校驗(yàn)成功脯宿,就讓請(qǐng)求繼續(xù)執(zhí)行念颈。首先從請(qǐng)求頭中提取出authorization字段,這個(gè)字段對(duì)應(yīng)的value就是用戶的token嗅绰。將提取出來(lái)的token字符串轉(zhuǎn)換為一個(gè)Claims對(duì)象舍肠,再?gòu)腃laims對(duì)象中提取出當(dāng)前用戶名和用戶角色,創(chuàng)建一個(gè)UsernamePasswordAuthenticationToken放到當(dāng)前的Context中窘面,然后執(zhí)行過(guò)濾鏈?zhǔn)拐?qǐng)求繼續(xù)執(zhí)行下去翠语。
public class JwtFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
String jwtToken = req.getHeader("authorization");
Jws<Claims> jws = Jwts.parser().setSigningKey("zby@123")
.parseClaimsJws(jwtToken.replace("Bearer", ""));
Claims claims = jws.getBody();
String username = claims.getSubject();
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(((String) claims.get("authorities")));
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(token);
filterChain.doFilter(servletRequest,servletResponse);
}
}
兩個(gè)過(guò)濾器配置好后,在Security配置類中添加兩個(gè)過(guò)濾器
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello")
.hasRole("user")
.antMatchers("/admin")
.hasRole("admin")
.antMatchers(HttpMethod.POST,"/login")
.permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtLoginFilter("/login",authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new JwtFilter(),UsernamePasswordAuthenticationFilter.class)
.csrf().disable();
}
配置路徑規(guī)則時(shí)财边, /hello 接口必須要具備 user 角色才能訪問(wèn)肌括, /admin 接口必須要具備 admin 角色才能訪問(wèn),POST 請(qǐng)求并且是 /login 接口則可以直接通過(guò)酣难,其他接口必須認(rèn)證后才能訪問(wèn)谍夭。
登陸成功,返回一個(gè)Json
{
"msg": "登錄成功",
"token": "eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfYWRtaW4sIiwic3ViIjoiYWRtaW4iLCJleHAiOjE1ODI3OTM0NjR9.4cTTZpjl1j2YxldmTHWbK6oN0htJn-kW9V2p6Nj7jc26znegUmtrXohy0dgH4uDH053UL4-IICSo_ETzJJtmeQ"
}
登錄成功后返回一個(gè)token憨募,請(qǐng)求資源時(shí)需要提供token才能正常訪問(wèn)