1. 程式人生 > 其它 >使用SpringBoot開發REST服務

使用SpringBoot開發REST服務

本文介紹如何基於Spring Boot搭建一個簡易的REST服務框架,以及如何通過自定義註解實現Rest服務鑑權

搭建框架

pom.xml

首先,引入相關依賴,資料庫使用mongodb,同時使用redis做快取

注意,這裡沒有使用tomcat,而是使用undertow
    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>

        <!--redis支援-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--mongodb支援-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
  • 引入spring-boot-starter-web支援web服務
  • 引入spring-boot-starter-data-redis 和spring-boot-starter-data-mongodb就可以方便的使用mongodb和redis了

配置檔案

profiles功能

為了方便 區分開發環境和線上環境,可以使用profiles功能,在application.properties裡增加 spring.profiles.active=dev

然後增加application-dev.properties作為dev配置檔案。

mondb配置

配置資料庫地址即可

spring.data.mongodb.uri=mongodb://ip:port/database?readPreference=primaryPreferred

redis配置

spring.redis.database=0  
# Redis伺服器地址
spring.redis.host=ip
# Redis伺服器連線埠
spring.redis.port=6379  
# Redis伺服器連線密碼(預設為空)
spring.redis.password=
# 連線池最大連線數(使用負值表示沒有限制)
spring.redis.pool.max-active=8  
# 連線池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.pool.max-wait=-1  
# 連線池中的最大空閒連線
spring.redis.pool.max-idle=8  
# 連線池中的最小空閒連線
spring.redis.pool.min-idle=0  
# 連線超時時間(毫秒)
spring.redis.timeout=0  

資料訪問

mongdb

mongdb訪問很簡單,直接定義介面extends MongoRepository即可,另外可以支援JPA語法,例如:

@Component
public interface UserRepository extends MongoRepository<User, Integer> {

    public User findByUserName(String userName);
}

使用時,加上@Autowired註解即可

@Component
public class AuthService extends BaseService {

    @Autowired
    UserRepository userRepository;
    }

Redis訪問

使用StringRedisTemplate即可直接訪問Redis

@Component
public class BaseService {
    @Autowired
    protected MongoTemplate mongoTemplate;

    @Autowired
    protected StringRedisTemplate stringRedisTemplate;

    }

儲存資料:

.stringRedisTemplate.opsForValue().set(token_key, user.getId()+"",token_max_age, TimeUnit.SECONDS);

刪除資料:

stringRedisTemplate.delete(getFormatToken(accessToken,platform));

Web服務

定義一個Controller類,加上RestController即可,使用RequestMapping用來設定url route

@RestController
public class AuthController extends BaseController {

    @RequestMapping(value = {"/"}, produces = "application/json;charset=utf-8", method = {RequestMethod.GET, RequestMethod.POST})
    @ResponseBody
    public String main() {
        return "hello world!";
    }

}

現在啟動,應該就能看到hello world!了

服務鑑權

簡易accessToken機制

提供登入介面,認證成功後,生成一個accessToken,以後訪問介面時,帶上accessToken,服務端通過accessToken來判斷是否是合法使用者。

為了方便,可以將accessToken存入redis,設定有效期。

        String token = EncryptionUtils.sha256Hex(String.format("%s%s", user.getUserName(), System.currentTimeMillis()));
        String token_key = getFormatToken(token, platform);
        this.stringRedisTemplate.opsForValue().set(token_key, user.getId()+"",token_max_age, TimeUnit.SECONDS);

攔截器身份認證

為了方便做統一的身份認證,可以基於Spring的攔截器機制,建立一個攔截器來做統一認證。

public class AuthCheckInterceptor implements HandlerInterceptor {
}

要使攔截器生效,還需要一步,增加配置:

@Configuration
public class SessionConfiguration extends WebMvcConfigurerAdapter {

    @Autowired
    AuthCheckInterceptor authCheckInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        super.addInterceptors(registry);
        // 新增攔截器
        registry.addInterceptor(authCheckInterceptor).addPathPatterns("/**");
    }
}

自定義認證註解

為了精細化許可權認證,比如有的介面只能具有特定許可權的人才能訪問,可以通過自定義註解輕鬆解決。在自定義的註解裡,加上roles即可。

/**
 *  許可權檢驗註解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthCheck {

    /**
     *  角色列表
     * @return
     */
    String[] roles() default {};
}

檢驗邏輯:

  • 只要介面加上了AuthCheck註解,就必須是登陸使用者
  • 如果指定了roles,則除了登入外,使用者還應該具備相應的角色。
    String[] ignoreUrls = new String[]{
            "/user/.*",
            "/cat/.*",
            "/app/.*",
            "/error"
    };
 public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {

        // 0 檢驗公共引數
        if(!checkParams("platform",httpServletRequest,httpServletResponse)){
            return  false;
        }

        // 1、忽略驗證的URL
        String url = httpServletRequest.getRequestURI().toString();
        for(String ignoreUrl :ignoreUrls){
            if(url.matches(ignoreUrl)){
                return true;
            }
        }

        // 2、查詢驗證註解
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        // 查詢註解
        AuthCheck authCheck = method.getAnnotation(AuthCheck.class);
        if (authCheck == null) {
            // 無註解,不需要
            return true;
        }

        // 3、有註解,先檢查accessToken
        if(!checkParams("accessToken",httpServletRequest,httpServletResponse)){
            return  false;
        }
        // 檢驗token是否過期
        Integer userId = authService.getUserIdFromToken(httpServletRequest.getParameter("accessToken"),
                httpServletRequest.getParameter("platform"));
        if(userId==null){
            logger.debug("accessToken timeout");
            output(ResponseResult.Builder.error("accessToken已過期").build(),httpServletResponse);
            return false;
        }

        // 4、再檢驗是否包含必要的角色
        if(authCheck.roles()!=null&&authCheck.roles().length>0){
            User user = authService.getUser(userId);
            boolean isMatch = false;
            for(String role : authCheck.roles()){
                if(user.getRole().getName().equals(role)){
                    isMatch =  true;
                    break;
                }
            }
            // 角色未匹配,驗證失敗
            if(!isMatch){
                return false;
            }
        }

        return true;
    }

服務響應結果封裝

增加一個Builder,方便生成最終結果

public class ResponseResult {

    public static class Builder{
        ResponseResult responseResult;

        Map<String,Object> dataMap = Maps.newHashMap();

        public Builder(){
            this.responseResult = new ResponseResult();
        }

        public Builder(String state){
            this.responseResult = new ResponseResult(state);
        }


        public static Builder newBuilder(){
           return new Builder();
        }

        public static Builder success(){
            return new Builder("success");
        }

        public static Builder error(String message){
            Builder builder =  new Builder("error");
            builder.responseResult.setError(message);
            return builder;
        }

        public  Builder append(String key,Object data){
            this.dataMap.put(key,data);
            return this;
        }

        /**
         *  設定列表資料
         * @param datas 資料
         * @return
         */
        public  Builder setListData(List<?> datas){
            this.dataMap.put("result",datas);
            this.dataMap.put("total",datas.size());
            return this;
        }

        public  Builder setData(Object data){
            this.dataMap.clear();
            this.responseResult.setData(data);
            return this;
        }

        boolean wrapData = false;

        /**
         * 將資料包裹在data中
         * @param wrapData
         * @return
         */
        public  Builder wrap(boolean wrapData){
            this.wrapData = wrapData;
            return this;
        }

        public String build(){

            JSONObject jsonObject = new JSONObject();
            jsonObject.put("state",this.responseResult.getState());
            if(this.responseResult.getState().equals("error")){
                jsonObject.put("error",this.responseResult.getError());
            }
            if(this.responseResult.getData()!=null){
                jsonObject.put("data", JSON.toJSON(this.responseResult.getData()));
            }else  if(dataMap.size()>0){
                if(wrapData) {
                    JSONObject data = new JSONObject();
                    dataMap.forEach((key, value) -> {
                        data.put(key, value);
                    });
                    jsonObject.put("data", data);
                }else{
                    dataMap.forEach((key, value) -> {
                        jsonObject.put(key, value);
                    });
                }
            }
            return jsonObject.toJSONString();
        }

    }

    private String state;
    private Object data;
    private String error;


    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }

    public ResponseResult(){}

    public ResponseResult(String rc){
        this.state = rc;
    }

    /**
     * 成功時返回
     * @param rc
     * @param result
     */
    public ResponseResult(String rc, Object result){
        this.state = rc;
        this.data = result;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

}

呼叫時可以優雅一點

    @RequestMapping(value = {"/user/login","/pc/user/login"}, produces = "application/json;charset=utf-8", method = {RequestMethod.GET, RequestMethod.POST})
    @ResponseBody
    public String login(String userName,String password,Integer platform) {
        User user = this.authService.login(userName,password);
        if(user!=null){
            //  登陸
            String token = authService.updateToken(user,platform);
            return ResponseResult.Builder
                     .success()
                    .append("accessToken",token)
                    .append("userId",user.getId())
                    .build();
        }
        return ResponseResult.Builder.error("使用者不存在或密碼錯誤").build();
    }
    
    protected String error(String message){
        return  ResponseResult.Builder.error(message).build();
    }

    protected String success(){
        return  ResponseResult.Builder
                .success()
                .build();
    }

    protected String successDataList(List<?> data){
        return ResponseResult.Builder
                .success()
                .wrap(true) // data包裹
                .setListData(data)
                .build();
    }

作者:Jadepeng 出處:jqpeng的技術記事本--http://www.cnblogs.com/xiaoqi 您的支援是對博主最大的鼓勵,感謝您的認真閱讀。 本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。