spring boot rest 接口集成 spring security(2) - JWT配置



Spring Boot 集成教程


在教程 [spring boot rest 接口集成 spring security(1) - 最簡配置] 里介紹了最簡集成spring security的過程,本文將繼續介紹spring boot項目中集成spring security以及配置jwt的過程。

如果不了解jwt,可以參考5分鍾搞懂:JWT(Json Web Token)

項目內容

本文將通過創建一個實際的spring boot項目來演示spring security及jwt的配置過程,項目主要內容:

  • 集成spring security;
  • 配置jwt;
  • 加載用戶信息;
  • 實現幾個接口,配置訪問權限;
  • 最后通過Postman測試接口;

要求

  • JDK1.8或更新版本
  • Eclipse開發環境

如沒有開發環境,可參考前面章節 [spring boot 開發環境搭建(Eclipse)]。

項目創建

創建spring boot項目

打開Eclipse,創建spring boot的spring starter project項目,選擇菜單:File > New > Project ...,彈出對話框,選擇:Spring Boot > Spring Starter Project,在配置依賴時,勾選web, security,完成項目創建。

image

項目依賴

要使用jwt,引入jwt jar包

		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
		<dependency>
		    <groupId>io.jsonwebtoken</groupId>
		    <artifactId>jjwt</artifactId>
		    <version>0.9.1</version>
		</dependency>

項目配置

application.properties配置

## 服務器端口,如果不配置默認是8080端口
server.port=8096 

## jwt配置
#  簽名密鑰
jwt.secret=my_secret_2019
# jwt有效期(秒)
jwt.expiration=1800

代碼實現

項目目錄結構如下圖,我們添加了幾個類,下面將詳細介紹。

image

spring security的配置:SecurityConfig.java

這是spring security的java配置類,幾個主要的配置:

  • 用戶信息加載配置
  • 權限不足處理配置
  • 權限配置
  • jwt過濾器配置
  • 其他如密碼加密,CORS等配置

@Configuration
@EnableWebSecurity // 添加security過濾器
@EnableGlobalMethodSecurity(prePostEnabled = true) // 可以在controller方法上配置權限
public class SecurityConfig extends WebSecurityConfigurerAdapter{
    
	// 加載用戶信息
    @Autowired
    private UserDetailsService myUserDetailsService;
    
    // 權限不足錯誤信息處理,包含認證錯誤與鑒權錯誤處理
    @Autowired
    private JwtAuthError myAuthErrorHandler;
    
	// 密碼明文加密方式配置
    @Bean
    public PasswordEncoder myEncoder() {
      return new BCryptPasswordEncoder();
    }
    
    // jwt校驗過濾器,從http頭部Authorization字段讀取token並校驗
    @Bean
    public JwtAuthFilter myAuthFilter() throws Exception {
        return new JwtAuthFilter();
    }
    
    // 獲取AuthenticationManager(認證管理器),可以在其他地方使用
	@Bean(name="authenticationManagerBean")
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}
    
    // 認證用戶時用戶信息加載配置,注入myUserDetailsService
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
    	auth.userDetailsService(myUserDetailsService);
    }
    
    // 配置http,包含權限配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	http

    	// 由於使用的是JWT,我們這里不需要csrf
    	.csrf().disable()

    	// 基於token,所以不需要session
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

        // 設置myUnauthorizedHandler處理認證失敗、鑒權失敗
        .exceptionHandling().authenticationEntryPoint(myAuthErrorHandler).accessDeniedHandler(myAuthErrorHandler).and()

        // 設置權限
        .authorizeRequests()

        // 需要登錄
        .antMatchers("/hello/hello1").authenticated()

         // 需要角色權限
        .antMatchers("/hello/hello2").hasRole("ADMIN")

        // 除上面外的所有請求全部放開
        .anyRequest().permitAll();

    	// 添加JWT過濾器,JWT過濾器在用戶名密碼認證過濾器之前
    	http.addFilterBefore(myAuthFilter(), UsernamePasswordAuthenticationFilter.class);

        // 禁用緩存
//    	http.headers().cacheControl();  
    }
    
    // 配置跨源訪問(CORS)
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
}

用戶信息及用戶信息服務:AuthUser.java,AuthUserService.java

加載用戶信息,需要用戶信息類及用戶信息服務類。AuthUser繼承spring的UserDetails,必須重寫UserDetails的一些標准接口。注意與實體類User區別。


public class AuthUser implements UserDetails {

	private static final long serialVersionUID = -2336372258701871345L;
	
	//用戶實體類
	private User user;
	
	public AuthUser(User user) {
		this.setUser(user);
	}
	
	public static Collection<? extends GrantedAuthority> getAuthoritiesByRole(String role) {
		Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
		
		List<String> roles = Arrays.asList(role.split(","));
		if (roles.contains("user")) {
			authorities.add(new SimpleGrantedAuthority("ROLE_USER")); 
		}
		if (roles.contains("admin")) {
			authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); 
		} 

		return authorities;
	}
	
	// 提供權限信息
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {

		return getAuthoritiesByRole(getUser().getRole());
	}

	// 提供賬號名稱
	@Override
	public String getUsername() {
		return getUser().getMobile();
	}

	// 提供密碼
	@Override
	public String getPassword() {
		return getUser().getPassword();
	}

	// 賬號是否沒過期,過期的用戶無法認證
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	// 賬號是否沒鎖住,鎖住的用戶無法認證
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	// 密碼是否沒過期,密碼過期的用戶無法認證
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	// 用戶是否使能,未使能的用戶無法認證
	@Override
	public boolean isEnabled() {
		return true;
	}

	public User getUser() {
		return user;
	}

	public void setUser(User user) {
		this.user = user;
	}

}

AuthUserService繼承UserDetailsService,重寫了加載用戶信息接口:

@Service
public class AuthUserService implements UserDetailsService {

	// 加載用戶信息
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		
		// 此處應從數據庫加載用戶信息,為簡便起見,直接創建一個用戶
		// password的值:$2a$10$EmsokMb6Vkav7m61kY0PtO.ZCLe0h.uJqVAZW7YYBpSUxd/DMkZuG,
		// 是明文123456使用BCryptPasswordEncoder加密的值
		User user = new User(1l, "abc1", username, "$2a$10$EmsokMb6Vkav7m61kY0PtO.ZCLe0h.uJqVAZW7YYBpSUxd/DMkZuG", "user");
		AuthUser authUser = new AuthUser(user);
		
		return (UserDetails) authUser;
	}
}

認證失敗、鑒權失敗處理:JwtAuthError.java

當認證失敗,系統會拋出認證失敗異常,可以配置我們自己的認證失敗處理類,同樣鑒權失敗也可以配置我們自己的失敗處理類。

JwtAuthError繼承AuthenticationEntryPoint(認證失敗接口)、AccessDeniedHandler(鑒權失敗接口),重寫了這2個接口類的失敗處理方法,其實JwtAuthError可以分為2個類,我們合二為一了。

@Component
public class JwtAuthError implements AuthenticationEntryPoint, AccessDeniedHandler {

	@SuppressWarnings("unused")
	private static final org.slf4j.Logger log = LoggerFactory.getLogger(JwtAuthError.class);

	// 認證失敗處理,返回401 json數據
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
    	
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write("{\"status\":401,\"message\":\"Unauthorized or invalid token\"}");
    	
    }
    
    // 鑒權失敗處理,返回403 json數據
	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
			AccessDeniedException accessDeniedException) throws IOException, ServletException {
		
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write("{\"status\":403,\"message\":\"Forbidden\"}");
	}
}

JWT過濾器

JWT過濾器每次請求應該只執行一次,所以繼承OncePerRequestFilter,JWT過濾器的主要行為:

  • 對於每次請求,從http頭部Authorization字段中讀取jwt
  • 嘗試解密jwt,如果正常解出,說明是合法用戶
  • 如果是合法用戶,設置認證信息,認證通過

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

	private static final org.slf4j.Logger log = LoggerFactory.getLogger(JwtAuthFilter.class);

    @Autowired
    private JwtUtil jwtUtil;

    private String tokenHeader="Authorization";

    private String tokenPrefix="Bearer";

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain) throws ServletException, IOException {
    	
    	// 從http頭部讀取jwt
        String authHeader = request.getHeader(this.tokenHeader);
        if (authHeader != null && authHeader.startsWith(tokenPrefix)) {
	        
	        final String authToken = authHeader.substring(tokenPrefix.length() + 1); // The part after "Bearer "
	        String username = null, role = null;
	        
	        // 從jwt中解出賬號與角色信息
	        try {
	        	username = jwtUtil.getUsernameFromToken(authToken);
	        	role = jwtUtil.getClaimFromToken(authToken, "role", String.class);
	        } catch (Exception e) {
	        	log.debug("異常詳情", e);
	        	log.info("無效token");
	        }
	        
	        // 如果jwt正確解出賬號信息,說明是合法用戶,設置認證信息,認證通過
	        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
	        	
	            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
	            		username, null, AuthUser.getAuthoritiesByRole(role));
	            
	            // 把請求的信息設置到UsernamePasswordAuthenticationToken details對象里面,包括發請求的ip等
	            auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
	            
	            // 設置認證信息
	            SecurityContextHolder.getContext().setAuthentication(auth);
		        
	        }
        }
            
        // 調用下一個過濾器
        chain.doFilter(request, response);
    }
}

User實體類(model層)

User實體類對應於數據庫中的User表(我們簡化了,沒有連數據庫)


public class User {
    private Long id;

    private String nickname;

    private String mobile;

    private String password;

    private String role;

    public User(Long id, String nickname, String mobile, String password, String role) {
        this.id = id;
        this.nickname = nickname;
        this.mobile = mobile;
        this.password = password;
        this.role = role;
    }

    public User() {
        super();
    }
}

LoginRequest類(model層)

登錄請求類,這個類將會接受並校驗用戶登錄時輸入的賬號密碼,關於輸入校驗,可以參考 [spring boot輸入數據校驗(validation)]


public class LoginRequest {
	
	@SuppressWarnings("unused")
	private static final org.slf4j.Logger log = LoggerFactory.getLogger(LoginRequest.class);
	
	@NotNull(message="賬號必須填")
	@Pattern(regexp = "^[1]([3][0-9]{1}|59|58|88|89)[0-9]{8}$", message="賬號請輸入11位手機號") // 手機號
	private String account;
	
    @NotNull(message="密碼必須填")
    @Size(min=6, max=16, message="密碼6~16位")
	private String password;
    
	private boolean rememberMe;
	
	public String getAccount() {
		return account;
	}
	public void setAccount(String account) {
		this.account = account;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	public boolean isRememberMe() {
		return rememberMe;
	}
	public void setRememberMe(boolean rememberMe) {
		this.rememberMe = rememberMe;
	}
	
}

AuthController類(控制層)

AuthController類實現了2個REST API:

  • login - 用戶提供賬號密碼,如果密碼正確,返回token,否則返回賬號或密碼錯誤提示;
  • refresh 輸入一個合法的舊token,返回新token

@RestController
@RequestMapping("/auth")
public class AuthController {
	
    @Autowired
    private AuthService authService;
    
	/**
	 * login 
	 * @param authRequest
	 * @param bindingResult
	 * @return ResponseEntity<Result> 
	 */
    @RequestMapping(value = "/login", method = RequestMethod.POST, produces="application/json")
    public ResponseEntity<Result> login(@Valid @RequestBody LoginRequest authRequest, BindingResult bindingResult) throws AuthenticationException{
    	
		if(bindingResult.hasErrors()) {			
			Result res = MiscUtil.getValidateError(bindingResult);
			return new ResponseEntity<Result>(res, HttpStatus.UNPROCESSABLE_ENTITY);
		}
    	
        final String token = authService.login(authRequest.getAccount(), authRequest.getPassword());
        
        // Return the token
        Result res = new Result(200, "ok");
        res.putData("token", token);
        return ResponseEntity.ok(res);
    }
    
	/**
	 * refresh 
	 * @param request
	 * @return ResponseEntity<Result> 
	 */
    @RequestMapping(value = "/refresh", method = RequestMethod.GET, produces="application/json")
    public ResponseEntity<Result> refresh(HttpServletRequest request, @RequestParam String token) throws AuthenticationException{
    	
    	Result res = new Result(200, "ok");
    	
    	String refreshedToken = authService.refresh(token);
        
        if(refreshedToken == null) {
        	res.setStatus(400);
        	res.setMessage("無效token");
            return new ResponseEntity<Result>(res, HttpStatus.BAD_REQUEST);
        } 
        
        
        res.putData("token", token);
        return ResponseEntity.ok(res);
    }
	
}

HelloController類(控制層)

實現了3個REST API:

  • hello1
  • hello2
  • hello3

用於測試權限配置


@RestController
@RequestMapping("/hello")
public class HelloController {
	
	@RequestMapping(value="/hello1", method=RequestMethod.GET)
    public String hello1() {
	        
        return "Hello1!";
    }
	
	@RequestMapping(value="/hello2", method=RequestMethod.GET)
    public String hello2() {
	        
        return "Hello2!";
    }
	
	@RequestMapping(value="/hello3", method=RequestMethod.GET)
    public String hello3() {
	        
        return "Hello3!";
    }
}

AuthService接口與AuthServiceImpl實現類(服務層)

AuthService提供對AuthController的服務

AuthService.java

public interface AuthService {
    User register(User userToAdd);
    String login(String username, String password);
    String refresh(String oldToken);
}

AuthServiceImpl.java


@Service
public class AuthServiceImpl implements AuthService {
	
	private static final org.slf4j.Logger log = LoggerFactory.getLogger(AuthServiceImpl.class);

    private AuthenticationManager authenticationManager;
    private UserDetailsService userDetailsService;
    private JwtUtil jwtUtil;

    @Autowired
    public AuthServiceImpl(
            AuthenticationManager authenticationManager,
            UserDetailsService userDetailsService,
            JwtUtil jwtUtil) {
        this.authenticationManager = authenticationManager;
        this.userDetailsService = userDetailsService;
        this.jwtUtil = jwtUtil;
    }

    @Override
    public User register(User userToAdd) {
    	// TODO: 保存user到數據庫
        return null;
    }

    @Override
    public String login(String username, String password) {
    	// 認證用戶,認證失敗拋出異常,由JwtAuthError的commence類返回401
        UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
        final Authentication authentication = authenticationManager.authenticate(upToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        // 如果認證通過,返回jwt
    	final AuthUser userDetails = (AuthUser) userDetailsService.loadUserByUsername(username);
        final String token = jwtUtil.generateToken(userDetails.getUser());
        return token;
    }

    @Override
    public String refresh(String oldToken) {
        String newToken = null;
        
        try {
        	newToken = jwtUtil.refreshToken(oldToken);
        } catch (Exception e) {
        	log.debug("異常詳情", e);
        	log.info("無效token");
        }
		return newToken;
    }
}

其他

剩下的一些類

  • Result.java 結果封裝類
  • MiscUtil.java 輔助類
  • JwtUtil.java jwt處理類,加密解密等操作

運行

Eclipse左側,在項目根目錄上點擊鼠標右鍵彈出菜單,選擇:run as -> spring boot app 運行程序。 打開Postman訪問接口,運行結果如下:

訪問/hello/hello1接口,需要登錄訪問,沒有帶上token,返回401

image

登錄獲取token

image

再次訪問需要登錄訪問的/hello/hello1接口,帶上token,可以看到訪問成功

image

訪問需要admin權限的/hello/hello2接口,雖然帶上token,但權限不足,可以看到返回403

image

總結

完整代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM