基於Spring Security和 JWT的權限系統設計

寫在前面

  • 關於 Spring Security Web系統的認證和權限模塊也算是一個系統的基礎設施了,幾乎任何的互聯網服務都會涉及到這方面的要求。在Java EE領域,成熟的安全框架解決方案一般有 Apache Shiro、Spring Security等兩種技術選型。Apache Shiro簡單易用也算是一大優勢,但其功能還是遠不如 Spring Security強大。Spring Security可以為 Spring 應用提供聲明式的安全訪問控制,起通過提供一系列可以在 Spring應用上下文中可配置的Bean,並利用 Spring IoC和 AOP等功能特性來為應用系統提供聲明式的安全訪問控制功能,減少了諸多重複工作。
  • 關於JWT JSON Web Token (JWT),是在網絡應用間傳遞信息的一種基於 JSON的開放標準((RFC 7519),用於作為JSON對象在不同系統之間進行安全地信息傳輸。主要使用場景一般是用來在 身份提供者和服務提供者間傳遞被認證的用戶身份信息。關於JWT的科普,可以看看阮一峰老師的《JSON Web Token 入門教程》。

本文則結合 Spring Security和 JWT兩大利器來打造一個簡易的權限系統。

本文實驗環境如下:

  • Spring Boot版本:2.0.6.RELEASE
  • IDE:IntelliJ IDEA 2018.2.4

歡迎工作一到五年的Java工程師朋友們加入Java程序員開發: 854393687

群內提供免費的Java架構學習資料(裡面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!


設計用戶和角色

本文實驗為了簡化考慮,準備做如下設計:

  • 設計一個最簡角色表role,包括角色ID和角色名稱
基於Spring Security和 JWT的權限系統設計

  • 設計一個最簡用戶表user,包括用戶ID,用戶名,密碼
基於Spring Security和 JWT的權限系統設計

  • 再設計一個用戶和角色一對多的關聯表user_roles 一個用戶可以擁有多個角色
基於Spring Security和 JWT的權限系統設計



創建 Spring Security和 JWT加持的 Web工程

  • pom.xml 中引入 Spring Security和 JWT所必需的依賴
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-security/<artifactid>
/<dependency>
<dependency>
<groupid>io.jsonwebtoken/<groupid>
<artifactid>jjwt/<artifactid>
<version>0.9.0/<version>
/<dependency>
  • 項目配置文件中加入數據庫和 JPA等需要的配置
server.port=9991
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://121.196.XXX.XXX:3306/spring_security_jwt?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=XXXXXX
logging.level.org.springframework.security=info
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jackson.serialization.indent_output=true
  • 創建用戶、角色實體

用戶實體 User

/**
* @ www.codesheep.cn
* 20190312
*/
@Entity
public class User implements UserDetails {

@Id
@GeneratedValue
private Long id;
private String username;
private String password;
@ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER)
private List<role> roles;
...
// 下面為實現UserDetails而需要的重寫方法!
@Override
public Collection extends GrantedAuthority> getAuthorities() {
List<grantedauthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add( new SimpleGrantedAuthority( role.getName() ) );
}
return authorities;
}

...
}
/<grantedauthority>/<role>

此處所創建的 User類繼承了 Spring Security的 UserDetails接口,從而成為了一個符合 Security安全的用戶,即通過繼承 UserDetails,即可實現 Security中相關的安全功能。

角色實體 Role:

/**
* @ www.codesheep.cn
* 20190312
*/
@Entity
public class Role {
@Id
@GeneratedValue
private Long id;
private String name;

... // 省略 getter和 setter
}
  • 創建JWT工具類

主要用於對 JWT Token進行各項操作,比如生成Token、驗證Token、刷新Token等

/**
* @ www.codesheep.cn
* 20190312
*/
@Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -5625635588908941275L;
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
public String generateToken(UserDetails userDetails) {
...
}
String generateToken(Map<string> claims) {
...
}
public String refreshToken(String token) {
...
}
public Boolean validateToken(String token, UserDetails userDetails) {
...
}
... // 省略部分工具函數
}
/<string>
  • 創建Token過濾器,用於每次外部對接口請求時的Token處理
/**
* @ www.codesheep.cn
* 20190312
*/
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired

private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal ( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader( Const.HEADER_STRING );
if (authHeader != null && authHeader.startsWith( Const.TOKEN_PREFIX )) {
final String authToken = authHeader.substring( Const.TOKEN_PREFIX.length() );
String username = jwtTokenUtil.getUsernameFromToken(authToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
  • Service業務編寫

主要包括用戶登錄和註冊兩個主要的業務

public interface AuthService {
User register( User userToAdd );
String login( String username, String password );
}
/**
* @ www.codesheep.cn
* 20190312
*/
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired

private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserRepository userRepository;
// 登錄
@Override
public String login( String username, String password ) {
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password );
final Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
final UserDetails userDetails = userDetailsService.loadUserByUsername( username );
final String token = jwtTokenUtil.generateToken(userDetails);
return token;
}
// 註冊
@Override
public User register( User userToAdd ) {
final String username = userToAdd.getUsername();
if( userRepository.findByUsername(username)!=null ) {
return null;
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
final String rawPassword = userToAdd.getPassword();
userToAdd.setPassword( encoder.encode(rawPassword) );
return userRepository.save(userToAdd);
}
}
  • Spring Security配置類編寫(非常重要)

這是一個高度綜合的配置類,主要是通過重寫 WebSecurityConfigurerAdapter 的部分 configure配置,來實現用戶自定義的部分。

/**
* @ www.codesheep.cn
* 20190312
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired

private UserService userService;
@Bean
public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtTokenFilter();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure( AuthenticationManagerBuilder auth ) throws Exception {
auth.userDetailsService( userService ).passwordEncoder( new BCryptPasswordEncoder() );
}
@Override
protected void configure( HttpSecurity httpSecurity ) throws Exception {
httpSecurity.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // OPTIONS請求全部放行
.antMatchers(HttpMethod.POST, "/authentication/**").permitAll() //登錄和註冊的接口放行,其他接口全部接受驗證
.antMatchers(HttpMethod.POST).authenticated()
.antMatchers(HttpMethod.PUT).authenticated()
.antMatchers(HttpMethod.DELETE).authenticated()
.antMatchers(HttpMethod.GET).authenticated();
// 使用前文自定義的 Token過濾器
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
httpSecurity.headers().cacheControl();
}
}
  • 編寫測試 Controller

登錄和註冊的 Controller:

/**
* @ www.codesheep.cn
* 20190312
*/
@RestController
public class JwtAuthController {

@Autowired
private AuthService authService;
// 登錄
@RequestMapping(value = "/authentication/login", method = RequestMethod.POST)
public String createToken( String username,String password ) throws AuthenticationException {
return authService.login( username, password ); // 登錄成功會返回JWT Token給用戶
}
// 註冊
@RequestMapping(value = "/authentication/register", method = RequestMethod.POST)
public User register( @RequestBody User addedUser ) throws AuthenticationException {
return authService.register(addedUser);
}
}

再編寫一個測試權限的 Controller:

/**
* @ www.codesheep.cn
* 20190312
*/
@RestController
public class TestController {
// 測試普通權限
@PreAuthorize("hasAuthority('ROLE_NORMAL')")
@RequestMapping( value="/normal/test", method = RequestMethod.GET )
public String test1() {
return "ROLE_NORMAL /normal/test接口調用成功!";
}
// 測試管理員權限
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@RequestMapping( value = "/admin/test", method = RequestMethod.GET )
public String test2() {
return "ROLE_ADMIN /admin/test接口調用成功!";
}
}

這裡給出兩個測試接口用於測試權限相關問題,其中接口 /normal/test需要用戶具備普通角色(ROLE_NORMAL)即可訪問,而接口/admin/test則需要用戶具備管理員角色(ROLE_ADMIN)才可以訪問。

接下來啟動工程,實驗測試看看效果


實驗驗證

  • 在文章開頭我們即在用戶表 user中插入了一條用戶名為 codesheep的記錄,並在用戶-角色表 user_roles中給用戶 codesheep分配了普通角色(ROLE_NORMAL)和管理員角色(ROLE_ADMIN)
  • 接下來進行用戶登錄,並獲得後臺向用戶頒發的JWT Token
基於Spring Security和 JWT的權限系統設計

  • 接下來訪問權限測試接口

不帶 Token直接訪問需要普通角色(ROLE_NORMAL)的接口 /normal/test會直接提示訪問不通:

基於Spring Security和 JWT的權限系統設計

而帶 Token訪問需要普通角色(ROLE_NORMAL)的接口 /normal/test才會調用成功:

基於Spring Security和 JWT的權限系統設計

同理由於目前用戶具備管理員角色,因此訪問需要管理員角色(ROLE_ADMIN)的接口 /admin/test也能成功:

基於Spring Security和 JWT的權限系統設計

接下里我們從用戶-角色表裡將用戶codesheep的管理員權限刪除掉,再訪問接口 /admin/test,會發現由於沒有權限,訪問被拒絕了:

基於Spring Security和 JWT的權限系統設計

經過一系列的實驗過程,也達到了我們的預期!

歡迎工作一到五年的Java工程師朋友們加入Java程序員開發: 854393687

群內提供免費的Java架構學習資料(裡面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!


分享到:


相關文章: