JWT实作(四)(Day8)

今天要来实作登入功能~!!

这边要说说之前我刚开始看前後分离框架的不习惯,因为在之前没分离的开发情况下,
登入的转页是依靠後端的判断,但在前後分离的状况下,後端变成单纯提供资料的地方,
因此转导页逻辑就会由前端负责,所以後端再也不用烦恼要把页面导去哪里了,专心提供资料就可以了。

我们要如何判断该使用者的权限呢? 这边我会采取的方式就是去资料库根据使用者的帐号把资料捞出来,然後比对加密过後的密码是否一致,在给予一串JWT作为其之後请求其他连结的身分识别,这代表说除非身分识别过期,不然资料库其实就只要请求一次资料就好。

JSON Web Token for Java and Android(套件官网)

新增这些套件到pom.xml档

        <dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-api</artifactId>
			<version>0.11.2</version>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-impl</artifactId>
			<version>0.11.2</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-jackson</artifactId> 
			<version>0.11.2</version>
			<scope>runtime</scope>
		</dependency>

新增登入路径

package com.stockAPI.controller;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.stockAPI.model.APIReturnObject;
import com.stockAPI.model.StockUser;
import com.stockAPI.model.User;
import com.stockAPI.service.JWTService;
import com.stockAPI.service.StockUserService;

@RestController
@RequestMapping("user")
public class UserController {
	
	@Autowired
	StockUserService stockUserService;
	
	@Autowired
	JWTService jWTService;
	
	@GetMapping("testBlock")
	public String testBlock() {
		return "testBlock";
	}
	
	@GetMapping("testUnblock")
	public String testUnblock() {
		return "testUnblock";
	}
	
	@PostMapping("login")
	public APIReturnObject login(@RequestBody User user) {
		APIReturnObject result = new APIReturnObject();
		Map<String, Object> data = new HashMap<String, Object>();
		String token = jWTService.generateToken(user);
		result.setMessage("登入成功,取得token");
		data.put("token", token);
		result.setData(data);
		return result;
	}
	
	@GetMapping("search/{account}")
	public APIReturnObject search(@PathVariable(name="account") String account) {
		APIReturnObject result = new APIReturnObject();
		Map<String, Object> data = new HashMap<String, Object>();
		StockUser  stockUser = stockUserService.getOwnData(account);
		data.put("userData", stockUser.getUser());
		result.setMessage("用户资料查询成功");
		result.setData(data);
		return result;
		
	}
	
	@PostMapping("create")
	public APIReturnObject create(@RequestBody User user) {
		APIReturnObject result = new APIReturnObject();
		Map<String, Object> data = new HashMap<String, Object>();
		Integer user_id = stockUserService.addUser(user);
		data.put("user_id", user_id);
		result.setMessage("用户资料新增成功");
		result.setData(data);
		return result;
	}

}

开放登入路径给所有人登入

package com.stockAPI.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
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;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import com.stockAPI.service.StockUserService;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
	@Autowired
	StockUserService stockUserService;
	
	@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(stockUserService).
		passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	http
    		.authorizeRequests()
    		.antMatchers("/user/testUnblock").permitAll()
    		.antMatchers("/user/login").permitAll()
    		.antMatchers("/user/create").hasAuthority("ADMIN") //管理员可以新增使用者资料
    		.antMatchers("/user/search/**").permitAll() //大家都可以查询资料
    		.and()
    		.csrf().disable();
        
    }
    
    //加密器注册容器
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    //验证类别注册容器
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

新增JWTService

package com.stockAPI.service;

import java.security.Key;
import java.util.Calendar;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

import com.stockAPI.model.StockUser;
import com.stockAPI.model.User;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;

@Service
public class JWTService {
	
	@Autowired
    private AuthenticationManager authenticationManager;
	
	 private final String KEY = "StockAPIStockAPIStockAPIStockAPIStockAPIStockAPI";
	 
	 public String generateToken(User user) {
	        Authentication authentication =
            new UsernamePasswordAuthenticationToken(user.getAccount(), user.getPassword());
	        authentication = authenticationManager.authenticate(authentication);
	        StockUser userDetails = (StockUser) authentication.getPrincipal();

	        Calendar calendar = Calendar.getInstance();
	        calendar.add(Calendar.MINUTE, 20);

	        Claims claims = Jwts.claims();
		    claims.put("user_id", stockUser.getUser().getId());
			claims.put("account", stockUser.getUsername());
			claims.put("name", stockUser.getUser().getName());
			claims.put("authority", stockUser.getUser().getAuthority());
	        claims.setExpiration(calendar.getTime());
	        claims.setIssuer("KensStockAPI");

	        Key secretKey = Keys.hmacShaKeyFor(KEY.getBytes());

	        return Jwts.builder()
	                .setClaims(claims)
	                .signWith(secretKey)
	                .compact();
	    }
	 
	 public Map<String, Object> parseToken(String token) {
	        Key secretKey = Keys.hmacShaKeyFor(KEY.getBytes());

	        JwtParser parser = Jwts.parserBuilder()
	                .setSigningKey(secretKey)
	                .build();

	        Claims claims = parser
	                .parseClaimsJws(token)
	                .getBody();

	        return claims.entrySet().stream()
	                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
	    }

}

Expiration:设定过期时间(可以不加)
Issuer: JWT发行人(可以不加)

还有一些注册声明参数 (建议但不强制使用)

  • sub (Subject) - jwt所面向的用户
  • aud (Audience) - 接收jwt的一方
  • nbf (Not Before) - 定义在什麽时间之前,该jwt都是不可用的
  • iat (Issued At) - jwt的签发时间
  • jti (JWT ID) - jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

Authentication: spring security 提供的验证介面,他的功能是

1.承载欲验证资料(account/password)。
2.验证成功後,此物件会被存到SecurityContexts里面,之後你可以用SecurityContextHolder.getContext().getAuthentication() 获得 Authentication物件,
进而call getDetails方法取得使用者资讯。

/**
	 * The credentials that prove the principal is correct. This is usually a **password**,
	 * but could be anything relevant to the <code>AuthenticationManager</code>. Callers
	 * are expected to populate the credentials.
	 * @return the credentials that prove the identity of the <code>Principal</code>
	 */
	Object getCredentials();

/**
	 * Stores additional details about the authentication request. These might be an IP
	 * address, certificate serial number etc.
	 * @return additional details about the authentication request, or <code>null</code>
	 * if not used
	 */
	Object getDetails();

/**
	 * The identity of the principal being authenticated. In the case of an authentication
	 * request with username and password, this would be the **username**. Callers are
	 * expected to populate the principal for an authentication request.
	 * <p>
	 * The <tt>AuthenticationManager</tt> implementation will often return an
	 * <tt>Authentication</tt> containing richer information as the principal for use by
	 * the application. Many of the authentication providers will create a
	 * {@code UserDetails} object as the principal.
	 * @return the <code>Principal</code> being authenticated or the authenticated
	 * principal after authentication.
	 */
	Object getPrincipal();

UsernamePasswordAuthenticationToken继承了
AbstractAuthenticationToken(实作Authentication介面),
是security提供的简单验证类别。

此时看到这里不知你跟我是否有一样的疑问,为什麽authentication的用户资料是在Principal里面?

authentication = authenticationManager.authenticate(authentication);
StockUser userDetails = (StockUser) authentication.getPrincipal();

这是因为当我们使用UsernamePasswordAuthenticationToken时,
会调用 DaoAuthenticationProvider 协助验证,而 DaoAuthenticationProvider 继承自AbstractUserDetailsAuthenticationProvider 但是却没有实作验证方法。
所以我们其实是使用 AbstractUserDetailsAuthenticationProvider #authenticate 的方法。

public Authentication authenticate(Authentication authentication)
		throws AuthenticationException {
	String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
			: authentication.getName();
	user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
	preAuthenticationChecks.check(user);
	additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
	postAuthenticationChecks.check(user);
	Object principalToReturn = user;
	if (forcePrincipalAsString) {
		principalToReturn = user.getUsername();
	}
	return createSuccessAuthentication(principalToReturn, authentication, user);
}

此时可以注意forcePrincipalAsString预设是false,所以我们的principalToReturn才会是用户资料,不然就只会是username而已

接着让我们看一下最後关键的createSuccessAuthentication方法

/**
	 * Creates a successful {@link Authentication} object.
	 * <p>
	 * Protected so subclasses can override.
	 * </p>
	 * <p>
	 * Subclasses will usually store the original credentials the user supplied (not
	 * salted or encoded passwords) in the returned <code>Authentication</code> object.
	 * </p>
	 * @param principal that should be the principal in the returned object (defined by
	 * the {@link #isForcePrincipalAsString()} method)
	 * @param authentication that was presented to the provider for validation
	 * @param user that was loaded by the implementation
	 * @return the successful authentication token
	 */
	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user) {
		// Ensure we return the original credentials the user supplied,
		// so subsequent attempts are successful even with encoded passwords.
		// Also ensure we return the original getDetails(), so that future
		// authentication events after cache expiry contain the details
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
				authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());
		this.logger.debug("Authenticated user");
		return result;
	}

结论:

还记得我们在一开始的疑问— 为什麽principal会有用户资料?

原来在 #createSuccessAuthentication方法中,我们新建一个UsernamePasswordAuthenticationToken,
并且把用户资料当作principal,因此我们需要用getPrincipal()方法才能取得资料

接下来就到了我们的测试环节了


postman新增登入请求
https://ithelp.ithome.com.tw/upload/images/20210923/20138857WtqQMv9pHZ.png

发出请求~~ 成功取得JWT
https://ithelp.ithome.com.tw/upload/images/20210923/20138857POOrO1n5hl.png

好的,今天先到这边,下一篇会告诉大家如何利用JWT通过验证来获得特定连结的使用权


<<:  Day 11 - 成长曲线N+1 : 商业分析师

>>:  [Day09] Flutter with GetX gallery_saver 照片影片存到相簿

使用 VS code 开发

在 Visual Studio Code 开发 Sail 成功的帮我们把专案建立好了,可以来开始写程...

Progressive Web App Notifications API (21)

什麽是 Web Notifications API? 透过 Web Notifications AP...

Day 17 : PHP - MySQLi的面向过程和面向对象是什麽?又该如何做选择?

如标题,这篇想和大家聊聊MySQLi的「面向过程」和「面向对象」是什麽 我当初在学这里时,这个问题困...

提款、存款、汇款一次到位

提款 接下来,写个提款功能 # cogs/money.py @commands.command() ...

[D02]k8s基本名词认识-01

写在前面 今天加班,所以等等把文章顺过之後再补上,先发文 今天加班,所以等等把文章顺过之後再补上,先...