SpringSecurity+Oauth2+Jwt實現toekn認證和刷新token


簡單描述:最近在處理鑒權這一塊的東西,需求就是用戶登錄需要獲取token,然后攜帶token訪問接口,token認證成功接口才能返回正確的數據,如果訪問接口時候token過期,就采用刷新token刷新令牌(得到新的token和refresh_token),然后在訪問接口返回數據,如果刷新token也過期了,就提示用戶重新登錄。廢話不多說,直接上代碼。源碼在github上

使用 springboot + thymeleaf + mybatis 搭建的 

//核心依賴
<!-- spring security + OAuth2 + JWT    start  -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- spring security + OAuth2 + JWT end -->

 

//核心配置 application.yml文件
#toekn相關配置
token:
config:
#客戶端標識,類比為token的用戶名,我寫的是項目名
clientId: SOJ_DEMO
#客戶端安全碼,類比為token的密碼,我寫的是假郵箱
secret: soj@123
#表示授權模式: password(密碼模式),authorization_code(授權碼模式)
grantTypes: password
#表示權限范圍,該屬性為可選項
scopes: all
#令牌的有效時長,此處為180s/60,時長為2分鍾 設置短一點是為了測試刷新token
accessTokenValidity: 120
#刷新令牌的有效時長
refreshTokenValidity: 36000
#資源ID號
resourceId: SOJ_DEMO
#token簽名的key,用於token對稱加解密
signingKey: SOJ_SYMMETRY
#設置刷新令牌機制.true(重復使用:更新access_token時長后,refresh_toke時長不更新)。false(與true相反)
isRefreshToken: false

 

利用keytool工具生成密鑰對(非對稱秘鑰 公鑰私鑰) 來執行簽名過程  keytool工具使用幫助文檔 這里得好好看看 最好一步步來 繁瑣的過程,我的媽呀 真的要吐了 太惡心了

//在cmd命令提示符中執行此命令  綠色部分是變量 你可以修改
keytool -genkeypair -alias jwt -keyalg RSA -keypass xc1234 -keystore jwt.jks -storepass xc1234  

 執行完之后會在C:\Users\Administrator目錄下生成一個jwt.jks文件, .jks文件包含了我們的秘鑰 公鑰 私鑰,后續會在代碼中讀取此文件中的公鑰私鑰。

 把生成的jwt.jks文件放到resources目錄下的certificate文件夾中,這里給一下目錄結構

 

 然后在POM文件中加入對此文件的引入

//pom文件

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>

<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>certificate/*.jks</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>certificate/*.jks</include>
</includes>
</resource>
</resources>
</build>

 

 下邊開始代碼

JwtToken主要負責token解析和加解密 和上邊的jwt.jks文件打交道 讀取公鑰私鑰

package com.xc.soj_demo.jwt;

import io.jsonwebtoken.ClaimJwtException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Configuration;

import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;

/**
 * token加解密
 * 
 * 1.對稱加解密
 * 2.非對稱加解密(RSA)
 * 
 */
@Configuration
public class JwtToken {
    
    /**
     * 加載jwt.jks文件
     */
    private static InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("certificate/jwt.jks");
    private static PrivateKey privateKey = null;
    private static PublicKey publicKey = null;

    static {
        try {
            KeyStore keyStore = KeyStore.getInstance("JKS");
            keyStore.load(inputStream, "xc1234".toCharArray());
            privateKey = (PrivateKey) keyStore.getKey("jwt", "xc1234".toCharArray());
            publicKey = keyStore.getCertificate("jwt").getPublicKey();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 生成jwt token(非對稱加密模式 公鑰私鑰)
     */
    public static String generateTokenRSA(String subject, int expirationSeconds) {
        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() + expirationSeconds * 1000);
        return Jwts.builder()
                .setClaims(null)
                .setHeaderParam("typ", "JWT")
                .setSubject(subject)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.RS256, privateKey) 
                .compact();
    }

    /**
     * 解析jwt token(非對稱加密模式 公鑰私鑰)
     */
    public static Claims parseTokenRSA(String token) {
        if (StringUtils.isEmpty(token)) {
            return null;
        }
  
        try {
            return Jwts.parser()
                    .setSigningKey(publicKey)
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            System.out.println("try-catch:validate is token error ");
            return null;
        }
    }
    
    /**
     * token是否過期
     * @return  true:過期
     */
    public static boolean isTokenExpired(Date expiration) {
        boolean before = expiration.before(new Date());
        return before;
    }
    
    /**
     * 生成jwt token(對稱加密模式)
     */
    public static String generateToken(String subject, int expirationSeconds,
            String signingKey) {
        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() + expirationSeconds * 1000);
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(subject)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, signingKey)
                .compact();
    }
    
    /**
     * 解析jwt token(對稱加密模式)
     */
    public static String parseToken(String token,String signingKey) {
        if (StringUtils.isEmpty(token)) {
            return null;
        }
        token = StringUtils.substringAfter(token, "bearer");//定義token令牌的類型為bearer
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(signingKey.getBytes("UTF-8")).parseClaimsJws(token).getBody();
        } catch (ClaimJwtException e) {
            //源碼DefaultJwtParser.Class中的處理過程是 從token中取出載荷payload部分,解析出claim(claim中存在用戶信息),然后在解析是否過期,最后才拋出的異常
            //所以是可以從 ClaimJwtException e中取出需要的部分 並且源碼ClaimJwtException.Class類中有header和claim兩個私有屬性並提供了get方法
            claims = e.getClaims();
        } catch (UnsupportedEncodingException e) {
            return null;
        }
        String localUser = (String) claims.get("userinfo");// 拿到當前用戶
        return localUser;
    }
    
    
}
JwtToken.java

 AuthServerConfig主要負責配置令牌加載的屬性,自定義用戶信息到token令牌內

package com.xc.soj_demo.authenticationConfig;

import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

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

/**
 * OAuth2配置類
 * 
 * 1.配置令牌加載的屬性
 * 2.自定義用戶信息到token令牌內
 * 
 */
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private TokenConfig tokenConfig;
 
    /**
     * 注入authenticationManager
     * 來支持 password grant type
     */
    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 注入userDetailService
     * 來支持 refresh_token grant type
     * 人話講 就是toekn失效 需要用到refresh_token去重新請求 /oauth/token來簽發新的token和refresh_token
     */
    @Autowired
    private UserDetailsService userDetailService;
 
    /**
     * 定義oauth/token類接口信息
     * 
     * @description tokenConfig map
     *        map.get("clientId")    類比為token的用戶名
     *        map.get("secret")    類比為token的密碼
     *      map.get("grantTypes")表示授權類型 grant_type: password(密碼模式)
     *      map.get("scopes")權限范圍
     *      map.get("accessTokenValidity")token有效期
     *      map.get("refreshTokenValidity")刷新token有效時間
     *      map.get("resourceId")定義資源令牌頭部,資源服務器驗證令牌時用到
     *  
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        
        Map<String, String> map = tokenConfig.getConfig();
        clients.inMemory()
                .withClient(map.get("clientId"))
                .secret("{noop}" + map.get("secret"))
                .authorizedGrantTypes(map.get("grantTypes"), "refresh_token")
                .scopes(map.get("scopes"))
                .accessTokenValiditySeconds(Integer.parseInt(map.get("accessTokenValidity"))) 
                .refreshTokenValiditySeconds(Integer.parseInt(map.get("refreshTokenValidity")))
                .resourceIds(tokenConfig.getResourceId())
//                .authorities("ADMIN")
//                .redirectUris("http://localhost:8882/login") // 認證成功重定向URL
                .autoApprove(true);// 自動認證
        
    }
 
    /**
     * token令牌配置
     * 
     * @description 1.定義自定義token生成方式、tokenStore、、認證管理器
     *                 2.定義token加解密轉換器
     *                 3.定義token請求方式
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.accessTokenConverter(accessTokenConverter());
        endpoints.authenticationManager(authenticationManager);
        endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
        endpoints.userDetailsService(userDetailService);//支持refresh_token機制
        endpoints.reuseRefreshTokens(tokenConfig.isRefreshToken());//和配置文件對應的 具體看application.yml最后一項
    }
 
    /**
     * OAuth2服務配置
     * 
     * @description 1.允許/oauth/token被調用,默認deny
     *                 2.允許所有檢查token,默認deny。必須加,否則check_token不能訪問顯示401未授權錯誤
     *                 3.允許表單認證
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer
//            .tokenKeyAccess("permitAll()")
//            .checkTokenAccess("permitAll()")
            .allowFormAuthenticationForClients();
    }
 
    /**
     * 生成jwt令牌
     * @return
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
            /***
             * 重寫增強token方法,用於自定義一些token總需要封裝的信息
             * @return
             */
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {

                Authentication user = authentication.getUserAuthentication();
                String userName = user.getName();
                Collection<? extends GrantedAuthority> authority = user.getAuthorities();
                // 得到用戶名,去處理數據庫可以拿到當前用戶的信息和角色信息(需要傳遞到服務中用到的信息)
                final Map<String, Object> additionalInformation = new HashMap<>();
                // Map假裝用戶實體
                Map<String, Object> userinfo = new HashMap<>();
                userinfo.put("userId", "001");
                userinfo.put("username", userName);
                userinfo.put("authOrity", authority);
                additionalInformation.put("userinfo", JSON.toJSONString(userinfo));
                ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
                OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication);
                return enhancedToken;
            }
        };
        // 生成簽名的key,資源服務使用相同的字符達到一個對稱加密的效果,生產時候使用RSA非對稱加密方式
        accessTokenConverter.setSigningKey(tokenConfig.getSigningKey());
        return accessTokenConverter;
    }
    
    
}
AuthServerConfig.java

 OauthInterceprtor負責攔截oauth的異常,主要是token過期之后的處理,采用刷新令牌機制重新獲取token,再次請求資源

package com.xc.soj_demo.authenticationConfig;

import com.xc.soj_demo.constant.CodeConstant;
import com.xc.soj_demo.dao.UserDao;
import com.xc.soj_demo.entity.User;
import com.xc.soj_demo.jwt.JwtToken;
import com.xc.soj_demo.util.JsonUtil;
import net.sf.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

/**
 * OAuth2異常攔截類
 *
 * 1.對oauth錯誤異常進行攔截,這里主要針對令牌過期進行處理
 * 2.新的令牌與刷新令牌的存儲
 * 3.載入用戶信息到spring Security的ContextHolder中,保證后續url轉發
 * 4.刷新令牌過期后的返回狀態
 *
 */
public class OauthInterceptor extends OAuth2AuthenticationEntryPoint {

    @Value("${server.port}")
    private String port;

    @Autowired
    private TokenConfig tokenConfig;

    /**
     * 在啟動類中注入了restTemplate Bean
     */
    @Autowired
    RestTemplate restTemplate;

    @Resource
    private UserDao dao;

    @Autowired
    private AuthenticationManager authenticationManager;

    private WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        try {
            ResponseEntity<?> result = exceptionTranslator.translate(authException);
            JSONObject objBody = JSONObject.fromObject(result.getBody());
            String message = objBody.getString("message");

            //判斷是否為"訪問令牌過期",如果不是則以默認的方法繼續處理其他異常
            if (message.contains("Access token expired")) {

                //根據訪問令牌,解析出當前令牌用戶的用戶名稱,密碼等信息
                String localUser = JwtToken.parseToken(request.getHeader("Authorization"), tokenConfig.getSigningKey());
                @SuppressWarnings("unchecked")
                Map<String, Object> userMap = (Map<String, Object>) JsonUtil.json2Map(localUser);
                String username = (String)userMap.get("username");

                //根據用戶名稱,從數據庫獲取用戶的刷新令牌
                String refresh_token = dao.getRefreshToken(username);

                //獲取當前用戶信息
                User userObj  = dao.getUserByUserName(username);
                Map<String, Object> map = new HashMap<>();
                map.put("code", 1);//用戶存在 密碼正確
                map.put("userId", userObj.getUserId());
                map.put("username", userObj.getUsername());
                map.put("password", userObj.getPassword());
                List<String> listPermission = new ArrayList<>();
                listPermission.add("user::add");
                listPermission.add("user::list");
                listPermission.add("user::update");
                listPermission.add("user::delete");

                List<String> listRole = new ArrayList<>();
//                listRole.add("sys_admin");
//                listRole.add("admin");
                listRole.add(userObj.getUserRole());

                map.put("authOrity", listRole);
                map.put("userPermission", listPermission);

                //獲取OAuth2框架的配置信息,用於訪問刷新令牌接口
                Map<String, String> tokenMap = tokenConfig.getConfig();
                Map<String,String> mapParam = new HashMap<>();
                mapParam.put("username", userObj.getUsername());
                mapParam.put("password", userObj.getPassword());
                mapParam.put("client_id", tokenMap.get("clientId"));
                mapParam.put("client_secret", tokenMap.get("secret"));
                mapParam.put("grant_type", "refresh_token");//這里沒有寫錯 采用刷新令牌的方式
                mapParam.put("refresh_token", refresh_token);
                try {

                    @SuppressWarnings("unchecked")
                    Map<String, String> mapResult = restTemplate
                            .getForObject(
                                    "http://localhost:"+port+"/oauth/token?username={username}&password={password}&client_id={client_id}&client_secret={client_secret}&grant_type={grant_type}&refresh_token={refresh_token}",
                                    Map.class, mapParam);
                    // 如果刷新成功 跳轉到原來需要訪問的頁面
                    //寫入用戶信息到公共變量中,寫入信息到SecurityContext中
                    CodeConstant.USER_MAP = map;
                    List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
                    for (String role : listRole) {
                        grantedAuthorityList.add(new SimpleGrantedAuthority(
                                role));
                    }
                    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                            userObj.getUsername(), userObj.getPassword(), grantedAuthorityList);
                    Authentication authentications = authenticationManager
                            .authenticate(authRequest);
                    SecurityContextHolder.getContext().setAuthentication(
                            authentications);

                    response.setHeader("access_token",
                            mapResult.get("access_token"));
//                    response.setHeader("refresh_token",
//                            mapResult.get("refresh_token"));

                    //把新獲取到的refresh_token存到數據庫
                    dao.setRefreshToken(userObj.getUserId(),mapResult.get("access_token"));
                    response.setHeader("isRefreshToken", "yes");
                    request.getRequestDispatcher(request.getRequestURI())
                            .forward(request, response);
                } catch (Exception e) {
//                    e.printStackTrace();
                    // 獲取刷新令牌失敗時(刷新令牌過期時),返回指定格式的錯誤信息
                    response.setHeader("Content-Type", "application/json;charset=utf-8");
                    response.getWriter().print("{\"code\":411,\"message\":\"刷新令牌以過期,需要重新登錄.\"}");
                    response.getWriter().flush();
                }
            }else{
                super.commence(request,response,authException);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
OauthInterceptor.java

 ResourceConfiguration資源服務器配置,具體負責后台 哪些接口放開,哪些接口攔截

package com.xc.soj_demo.authenticationConfig;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

/**
 * 資源服務器-配置類
 * 
 * 1.設置接口訪問權限
 * 2.token驗證
 * 
 */
@Configuration
@EnableResourceServer
public class ResourceConfiguration extends ResourceServerConfigurerAdapter {
    
    @Value("${token.resourceId}")
    private String resourceId;
 
    /**
     * 定義資源服務器接口訪問權限
     * 
     * @description 1.定義無權限接口
     *                 2.定義接口訪問權限為admin
     *                 3.定義接口訪問權限為sys_admin
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/order/*","/getToken","/parseToken","/sys/test","/sys/login","/sys/doLogin","/js/**").permitAll()// "/order/*"資源是開放的
            .and().authorizeRequests()
            .antMatchers(HttpMethod.OPTIONS).permitAll()
//            .antMatchers("/B").hasRole("admin")
//          .antMatchers("/admin").hasRole("sys_admin")
            .anyRequest().authenticated();

    }
    
    /**
     * 定義資源服務器解析協議表頭(需要與認證服務器定義的表頭一致)
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(resourceId).stateless(true);
        resources.authenticationEntryPoint(new OauthInterceptor());
    }


}
ResourceConfiguration.java

TokenConfig從配置文件讀取token的相關配置

package com.xc.soj_demo.authenticationConfig;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

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


@Component
@Data
@ConfigurationProperties(prefix="token")
public class TokenConfig {
    
    private Map<String, String> config = new HashMap<>();
    private String resourceId;
    private String signingKey;
    private boolean isRefreshToken;

}
TokenConfig.java

UserDetailService 用戶信息獲取

package com.xc.soj_demo.authenticationConfig;

import com.xc.soj_demo.constant.CodeConstant;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 用戶信息獲取(用戶名稱,密碼,權限)
 * 
 */
@Component
public class UserDetailService implements UserDetailsService {
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //這個地方可以通過username從數據庫獲取正確的用戶信息,包括密碼和權限等。
        // 從user獲取正確的用戶信息,包括密碼和權限等。
        Map<String, Object> user = CodeConstant.USER_MAP;
        if (user != null) {
            @SuppressWarnings("unchecked")
            List<String> authOrity = (List<String>) user.get("authOrity");
            String PASSWORD = "{noop}" + user.get("password").toString();
            List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
            for (String auth : authOrity) {
                grantedAuthorityList.add(new SimpleGrantedAuthority(auth));
            }
            return new User(username, PASSWORD, grantedAuthorityList);
        } else {
            throw new UsernameNotFoundException("用戶[" + username + "]不存在");
        }

    }
    
    
}
UserDetailService.java

WebSecurityConfig認證服務器配置

package com.xc.soj_demo.authenticationConfig;

import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;

/**
 * Security-配置類(認證服務器)
 * 
 * 1.配置請求URL的訪問策略
 * 2.自定義認證登錄頁面URL
 * 3.配置OAuth2密碼模式
 * 
 */
@EnableWebSecurity//開啟權限驗證
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)//通過表達式控制方法權限
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    /**
     * 配置訪問策略
     * 
     * @description 1.設置授權請求
     *                 2.自定義登錄界面
     *                 3.設置使用jwt,可以允許跨域
     */
     @Override
     protected void configure(HttpSecurity http) throws Exception {
            http.requestMatchers()
                .antMatchers("/login")
                .antMatchers("/oauth/**")
                .and().authorizeRequests()
                .anyRequest().authenticated()
                .and().formLogin().loginPage("/login").permitAll()
                .and().csrf().disable();
     }
     
    /**
     * 需要配置這個支持password模式 support password grant type
     * @return
     * @throws Exception
     */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
 
    
}
WebSecurityConfig.java

核心代碼到這里就結束了,下邊是業務代碼,和上邊的關聯起來 MVC那一套

常量類:

package com.xc.soj_demo.constant;

import java.util.Map;

public class CodeConstant {
    //為了減少查詢數據庫的次數,把用戶的一些信息暫時存放到這里
    public static Map<String, Object> USER_MAP = null;

}

Controller:

package com.xc.soj_demo.controller;

import com.xc.soj_demo.entity.User;
import com.xc.soj_demo.service.UserService;
import com.xc.soj_demo.util.JsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;


@Controller
@RequestMapping("/sys")
public class LoginController {

    private static final Logger logger = LoggerFactory.getLogger(LoginController.class);

    @Resource
    private UserService userService;

    //測試頁面
    @RequestMapping("/test")
    public String testThymeleaf(ModelMap model) {
        User user = new User();
        user.setUsername("蓋聶");
        user.setUserRole("大叔");
        model.addAttribute("user", user);
        return "/viewTest";
    }

    //登錄頁面
    @RequestMapping("/login")
    public String login(ModelMap model) {
        return "/login";
    }

    @RequestMapping(value = "/doLogin")
    @ResponseBody
    public String login(User user) {
        Map<String, Object> resultMap = userService.login(user.getUsername(), user.getPassword());
        String str = JsonUtil.map2Json(resultMap);
        logger.info(str);
        return str;
    }


    //測試頁面
    @RequestMapping("/getList")
    @ResponseBody
    public List<String> getList() {
        List<String> list = new ArrayList<>();
        list.add("aa");
        list.add("bb");

        return list;
    }
}
LoginController.java

Dao:

package com.xc.soj_demo.dao;

import org.apache.ibatis.annotations.Mapper;
import com.xc.soj_demo.entity.User;

import java.util.Map;

@Mapper
public interface UserDao {
    User getUserByUserName(String username);

    void addToken2User(Map<String, Object> map);

    void setRefreshToken(String userId, String refreshToken);

    String getPasswordByUserName(String username);

    String getRefreshToken(String username)throws Exception;

    String selectRefreshTokenByUserId(Integer userId);
}

實體類:

package com.xc.soj_demo.entity;


import lombok.Data;

@Data
public class User {

    private String userId;
    private String username;
    private String password;
    private String userPermission;
    private String userRole;
    private String token;
    private String refreshToken;

}
User.java

實現類:

package com.xc.soj_demo.service.impl;


import com.fasterxml.jackson.core.JsonProcessingException;
import com.xc.soj_demo.authenticationConfig.TokenConfig;
import com.xc.soj_demo.constant.CodeConstant;
import com.xc.soj_demo.dao.UserDao;
import com.xc.soj_demo.entity.User;
import com.xc.soj_demo.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service("userService")
public class UserServiceImpl implements UserService {

    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    @Value("${server.port}")
    private String port;

    @Autowired
    private UserDao dao;

    @Autowired
    HttpServletRequest request;

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    private TokenConfig tokenConfig;

    @Override
    public Map<String, Object> login(String username, String password) {
        Map<String, Object> map = new HashMap<>();
        User userObj = dao.getUserByUserName(username);
        // 判斷用戶是否存在
        if (null == userObj) {
            map.put("code", 1);// 用戶不存在
            return map;
        }
        // 判斷密碼是否正確
        if (!password.equals(userObj.getPassword())) {
            map.put("code", -1);// 密碼錯誤
            return map;
        }
        map.put("code", 0);// 用戶存在 密碼正確
        map.put("userId", userObj.getUserId());
        map.put("username", userObj.getUsername());
        String userId = userObj.getUserId();

//        List<String> listAuthority = dao.getUserPermissionByUserid(userId);
        //模擬 查詢用戶權限(查詢結果 可以對用戶進行增刪改查)
        List<String> listPermission = new ArrayList<>();
        listPermission.add("user::add");
        listPermission.add("user::list");
        listPermission.add("user::update");
        listPermission.add("user::delete");

//        List<String> listRole = dao.getUserRolesByUid(userId);
        //模擬 查詢用戶角色 (查詢結果 userObj具有系統管理員 普通管理員的角色)
        List<String> listRole = new ArrayList<>();
//        listRole.add("sys_admin");
//        listRole.add("admin");
        listRole.add(userObj.getUserRole());

        map.put("authOrity", listRole);
        map.put("userPermission", listPermission);
        map.put("password", password);
        try {
            map.put("access_token", getOAuthToken(map));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return map;
    }

    /**
     * 調用OAuth2的獲取令牌接口
     *
     * @description 1.將用戶信息存入公共map中 2.獲取訪問令牌 3.寫入"刷新令牌"到數據庫
     *
     */
    private String getOAuthToken(Map<String, Object> map) throws JsonProcessingException {
        CodeConstant.USER_MAP = map;

        Map<String, String> tokenMap = tokenConfig.getConfig();
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
        formData.add("username", map.get("username").toString());
        formData.add("password", map.get("password").toString());
        formData.add("client_id", tokenMap.get("clientId"));
        formData.add("client_secret", tokenMap.get("secret"));
        formData.add("grant_type", tokenMap.get("grantTypes"));

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        String urlStr = "http://localhost:" + port + "/oauth/token";
        Map<?, ?> resultMap = restTemplate.exchange(urlStr, HttpMethod.POST,
                new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();

        if (null != resultMap) {
            try {
                setRefreshToken(map.get("userId").toString(), resultMap.get("refresh_token").toString());
            } catch (Exception e) {
                e.printStackTrace();
            }
            return resultMap.get("access_token").toString();
        }
        return null;
    }

    /**
     * 更新用戶的刷新令牌
     *
     */
    public void setRefreshToken(String userId, String refreshToken) throws Exception {
        dao.setRefreshToken(userId, refreshToken);
    }

    public String getPasswordByUserName(String username) throws Exception {
        String password = dao.getPasswordByUserName(username);
        return password;
    }


    /**
     * @Description 根據用戶ID獲取刷新token
     * @param userId
     * @param refreshToken
     * @return Boolean
     * @author chao.song
     */
    public Boolean verdictRefreshTokenByUId(Integer userId, String refreshToken) {
        if (userId == null || userId < 0) {
            return false;
        }
        if (refreshToken.isEmpty()) {
            return false;
        }
        String baseRefreshToken = dao.selectRefreshTokenByUserId(userId);
        if (refreshToken.equals(baseRefreshToken)) {
            return true;
        }
        return false;
    }


}
UserServiceImpl.java

Json工具類

package com.xc.soj_demo.util;


import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class JsonUtil {

    private static final ObjectMapper mObjectMapper = new ObjectMapper();

    static {
        mObjectMapper.configure(Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER, true);
        mObjectMapper.configure(Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
        mObjectMapper.configure(Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        mObjectMapper.configure(Feature.ALLOW_SINGLE_QUOTES, true);
        mObjectMapper.configure(Feature.ALLOW_NUMERIC_LEADING_ZEROS, true);
        mObjectMapper.configure(Feature.ALLOW_NON_NUMERIC_NUMBERS, true);

        mObjectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
        mObjectMapper.configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, true);
        mObjectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
        mObjectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
        mObjectMapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, true);

        DateFormat myDateFormat = new SimpleDateFormat("yyyy-MM-DD hh:mm:ss");
        mObjectMapper.getSerializationConfig().with(myDateFormat);
        mObjectMapper.getDeserializationConfig().with(myDateFormat);
    }

    /**
     * parameters key
     */
    private static final String PARA_KEY = "parameters";

    /**
     * @param jsonString
     * @return
     */
    public static Map<?, ?> json2Map(String jsonString) {
        try {
            Map<?, ?> map = mObjectMapper.readValue(jsonString, Map.class);
            return map;
        } catch (JsonMappingException e) {
            e.printStackTrace();
            return null;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * @param jsonString
     * @return
     * @description
     */
    public static List<Map<?, ?>> json2MapOfArrayList(String jsonString) {
        try {
            JavaType javaType = getCollectionType(ArrayList.class, Map.class);
            @SuppressWarnings("unchecked")
            List<Map<?, ?>> arrayList = (List<Map<?, ?>>) mObjectMapper.readValue(jsonString, javaType);

            return arrayList;
        } catch (JsonMappingException e) {
            e.printStackTrace();
            return null;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * @param collectionClass
     * @param elementClasses
     * @return
     */
    public static JavaType getCollectionType(Class<?> collectionClass, Class<?>... elementClasses) {
        return mObjectMapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
    }

    /**
     * @param map
     * @return
     * @description map to json string
     */
    public static String map2Json(Map<?, ?> map) {
        try {
            String ret = "";
            ret = mObjectMapper.writeValueAsString(map);
            // remove all "\"
            ret = ret.replaceAll("\\\\", "");
            if (ret.contains("\"[")) {
                ret = ret.replaceAll("\"\\[", "\\[");
            }
            if (ret.contains("]\"")) {
                ret = ret.replaceAll("\\]\"", "\\]");
            }
            if (ret.contains("\"{")) {
                ret = ret.replaceAll("\"\\{", "\\{");
            }
            if (ret.contains("}\"")) {
                ret = ret.replaceAll("\\}\"", "\\}");
            }
            return ret;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * @param list
     * @return
     */
    public static String listMap2Json(List<Map<String, Object>> list) {
        try {
            String ret = "";
            ret = mObjectMapper.writeValueAsString(list);
            // remove all "\"
            ret = ret.replaceAll("\\\\", "");
            return ret;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * @param strContent
     * @return
     * @description
     */
    public static String getJsonString(String strContent) {
        String ret = "";

        if (!strContent.contains(PARA_KEY)) {
            return ret;
        }
        String regex = "parameters[\\s]+=";
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(strContent);
        if (m.find()) {
            strContent = strContent.replaceFirst(regex, "");
        } else {
            regex = "parameters=";
            p = Pattern.compile(regex);
            m = p.matcher(strContent);
            if (m.find()) {
                strContent = strContent.replaceFirst(regex, "");
            }
        }
        strContent = strContent.trim();
        return strContent;
    }

    /**
     * @param byteArray
     * @return
     * @description
     */
    public static String bytes2Hex(byte[] byteArray) {
        StringBuffer strBuf = new StringBuffer();
        for (int i = 0; i < byteArray.length; i++) {
            if (byteArray[i] >= 0 && byteArray[i] < 16) {
                strBuf.append("0");
            }
            strBuf.append(Integer.toHexString(byteArray[i] & 0xFF));
        }
        return strBuf.toString();
    }

    /**
     * method_name: mapFormatString2List
     * <p>
     * parameters: mapString format is: [ { id=1, time=2013-11-09 09:00:00 }, {
     * id=2, time=2013-11-10 09:00:00 } ]
     * <p>
     * <p>
     * return: List<Map<String, Object>>
     */
    public static List<Map<String, Object>> mapFormatString2List(String strContent) {
        List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
        String ret = "";
        String regex = "\\{[^}]+\\}"; // \\{[^}]+\\} {[^}]*}
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(strContent);
        while (m.find()) {
            ret = m.group();
            ret = ret.replaceAll("\\{", "");
            ret = ret.replaceAll("\\}", "");
            ret = ret.trim();
            Map<String, Object> map = transStringToMap(ret);
            list.add(map);
        }
        return list;
    }

    /**
     * method_name: transStringToMap
     * <p>
     * parameters: mapString format is: id=1, time=2013-11-09 09:00:00 (delim:",",
     * token:"=")
     * <p>
     * return: Map
     */
    public static Map<String, Object> transStringToMap(String mapString) {
        Map<String, Object> map = new HashMap<String, Object>();
        StringTokenizer items;
        for (StringTokenizer entrys = new StringTokenizer(mapString, ","); entrys.hasMoreTokens(); map
                .put(items.nextToken().trim(), items.hasMoreTokens() ? ((Object) (items.nextToken().trim())) : null)) {
            items = new StringTokenizer(entrys.nextToken(), "=");
        }
        return map;
    }

    /**
     * @param str
     * @return
     * @description trim
     */
    public static String trimAll(String str) {
        if (null == str || str.length() <= 0) {
            return str;

        } else {
            return str.replaceAll("^[ ]+|[ ]+$", "");
        }
    }

}
JsonUtil.java

mapper文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xc.soj_demo.dao.UserDao">
    <resultMap id="UserResultMap" type="com.xc.soj_demo.entity.User">
        <id property="userId" column="user_id"/>
        <result property="username" column="username"/>
        <result property="password" column="password"/>
        <result property="userPermission" column="user_permission"/>
        <result property="userRole" column="user_role"/>
        <result property="token" column="token"/>
        <result property="refreshToken" column="refresh_token" />
    </resultMap>

    <sql id="tableName">t_user</sql>

    <update id="addToken2User" parameterType="java.util.Map">
        update
        <include refid="tableName"/>
        set token = #{token} where user_id = #{userId}
    </update>

    <select id="getUserByUserName" parameterType="String" resultMap="UserResultMap">
        select * from
        <include refid="tableName"/>
        where username = #{username}
    </select>

    <!-- 獲取用戶密碼 -->
    <select id="getPasswordByUserName" parameterType="String" resultType="String">
        select password from <include refid="tableName" /> where username = #{username}
    </select>

    <!-- 更新刷新令牌 -->
    <update id="setRefreshToken">
        update <include refid="tableName" /> set refresh_token = #{refreshToken} where user_id = #{userId}
    </update>

    <!-- 獲取用戶的刷新令牌 -->
    <select id="getRefreshToken" parameterType="String" resultType="String">
        select refresh_token from <include refid="tableName" />  where username = #{username}
    </select>


    <select id="selectRefreshTokenByUserId" parameterType="Integer" resultType="String">
        select refresh_token from <include refid="tableName" /> where user_id = #{userId}
    </select>
</mapper>
UserMapper.xml

 

記得在主啟動類中把restTemplate注入進去,這個是要用到的

 

測試:

首先登陸獲取token

 

 拿到token后請求后台接口getList

 

 然后 等2分鍾  等token過期 再次請求getList

 

 

 

 可以看到 第一次使用token去請求的時間是 15:21:26  等token過期之后再請求的時間是15:24:32  在配置文件中配置的token有效期是2分鍾 刷新token時間長一點是10個小時,這時候請求也可以訪問的原因  后台的刷新tokne機制起了作用。請求接口的時候使用的token是紫色方框中的那個后台驗證 這個token已過期  然后刷新token機制開始工作。從已失效的tokne的載荷中 取出用戶名,然后根據用戶名查詢用戶信息,得到refresh_token,然后使用refresh_token請求/oauth/token 獲取新的tokne也就是綠色方框中的access_token,最后在接着訪問接口的url的到結果返回回來。刷新token機制 具體體現在OauthInterceptor.java中

 


免責聲明!

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



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