使用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 您的支援是對博主最大的鼓勵,感謝您的認真閱讀。 本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。