tips:這是實戰篇,默認各位看官具備相應的基礎(文中使用了Lombok插件,如果使用源碼請先安裝插件)
文章目錄
項目配置
依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.36</version>
</dependency>
<!-- druid數據庫連接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- http請求所需jar包 -->
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpcore -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.7</version>
</dependency>
<!-- Jcode2Session解密所需jar包 -->
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15</artifactId>
<version>1.46</version>
</dependency>
<!-- 注意導入xfire-all jar包會與spring衝突 -->
<dependency>
<groupId>org.codehaus.xfire</groupId>
<artifactId>xfire-all</artifactId>
<version>1.2.6</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
</exclusion>
</exclusions>
</dependency>
application.yml
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/db_XXX?characterEncoding=utf-8&useSSl=false
driver-class-name: com.mysql.jdbc.Driver
# 此處使用Druid數據庫連接池
type: com.alibaba.druid.pool.DruidDataSource
#監控統計攔截的filters
filters: stat,wall,log4j
#druid配置
#配置初始化大小/最小/最大
initialSize: 5
minIdle: 5
maxActive: 20
#獲取連接等待超時時間
maxWait: 60000
#間隔多久進行一次檢測,檢測需要關閉的空閒連接
timeBetweenEvictionRunsMillis: 60000
#一個連接在池中最小生存的時間
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
#打開PSCache,並指定每個連接上PSCache的大小。oracle設爲true,mysql設爲false。分庫分表較多推薦設置爲false
poolPreparedStatements: false
maxPoolPreparedStatementPerConnectionSize: 20
# 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
connectionProperties:
druid:
stat:
mergeSql: true
slowSqlMillis: 5000
http:
encoding:
charset: utf-8
force: true
enabled: true
redis:
host: 127.0.0.1
port: 6379
password: 123456
#mybatis是獨立節點,需要單獨配置
mybatis:
mapper-locations: classpath*:mapper/*.xml
type-aliases-package: com.lzw.security.entity
configuration:
map-underscore-to-camel-case: true
server:
port: 8080
tomcat:
uri-encoding: utf-8
servlet:
context-path: /
#自定義參數,可以遷移走
token:
#redis默認過期時間(2小時)(這是自定義的)(毫秒)
expirationMilliSeconds: 7200000
#微信相關參數
weChat:
#小程序appid
appid: aaaaaaaaaaaaaaaa
#小程序密鑰
secret: ssssssssssssssss
程序代碼
security相關
security核心配置類
import com.lzw.security.common.NoPasswordEncoder;
import com.lzw.security.filter.JwtAuthenticationTokenFilter;
import com.lzw.security.handler.*;
import com.lzw.security.service.SelfUserDetailsService;
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.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @author: jamesluozhiwei
* @description: security核心配置類
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//表示開啓全局方法註解,可在指定方法上面添加註解指定權限,需含有指定權限纔可調用(基於表達式的權限控制)
public class SpringSecurityConf extends WebSecurityConfigurerAdapter {
@Autowired
AjaxAuthenticationEntryPoint authenticationEntryPoint;//未登陸時返回 JSON 格式的數據給前端(否則爲 html)
@Autowired
AjaxAuthenticationSuccessHandler authenticationSuccessHandler; //登錄成功返回的 JSON 格式數據給前端(否則爲 html)
@Autowired
AjaxAuthenticationFailureHandler authenticationFailureHandler; //登錄失敗返回的 JSON 格式數據給前端(否則爲 html)
@Autowired
AjaxLogoutSuccessHandler logoutSuccessHandler;//註銷成功返回的 JSON 格式數據給前端(否則爲 登錄時的 html)
@Autowired
AjaxAccessDeniedHandler accessDeniedHandler;//無權訪問返回的 JSON 格式數據給前端(否則爲 403 html 頁面)
@Autowired
SelfUserDetailsService userDetailsService; // 自定義user
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // JWT 攔截器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 加入自定義的安全認證
//auth.authenticationProvider(provider);
auth.userDetailsService(userDetailsService).passwordEncoder(new NoPasswordEncoder());//這裏使用自定義的加密方式(不使用加密),security提供了 BCryptPasswordEncoder 加密可自定義或使用這個
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 請根據自身業務進行擴展
// 去掉 CSRF
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //關閉session管理,使用token機制處理
.and()
.httpBasic().authenticationEntryPoint(authenticationEntryPoint)
//.and().antMatcher("/login")
//.and().authorizeRequests().anyRequest().access("@rbacauthorityservice.hasPermission(request,authentication)")// 自定義權限校驗 RBAC 動態 url 認證
.and().authorizeRequests().antMatchers(HttpMethod.GET,"/test").hasAuthority("test:list")
.and().authorizeRequests().antMatchers(HttpMethod.POST,"/test").hasAuthority("test:add")
.and().authorizeRequests().antMatchers(HttpMethod.PUT,"/test").hasAuthority("test:update")
.and().authorizeRequests().antMatchers(HttpMethod.DELETE,"/test").hasAuthority("test:delete")
.and().authorizeRequests().antMatchers("/test/*").hasAuthority("test:manager")
.and().authorizeRequests().antMatchers("/login").permitAll() //放行login(這裏使用自定義登錄)
.and().authorizeRequests().antMatchers("/hello").permitAll();//permitAll表示不需要認證
//微信小程序登錄不給予賬號密碼,關閉
// .and()
//開啓登錄, 定義當需要用戶登錄時候,轉到的登錄頁面、這是使用security提供的formLogin,不需要自己實現登錄登出邏輯、但需要實現相關方法
// .formLogin()
// .loginPage("/test/login.html")//可不指定,使用security自帶的登錄頁面
// .loginProcessingUrl("/login") //登錄地址
// .successHandler(authenticationSuccessHandler) // 登錄成功處理
// .failureHandler(authenticationFailureHandler) // 登錄失敗處理
// .permitAll()
// .and()
// .logout()//默認註銷行爲爲logout
// .logoutUrl("/logout")
// .logoutSuccessHandler(logoutSuccessHandler)
// .permitAll();
// 記住我
// http.rememberMe().rememberMeParameter("remember-me")
// .userDetailsService(userDetailsService).tokenValiditySeconds(1000);
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // 無權訪問 JSON 格式的數據
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // JWT Filter
}
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults(){
return new GrantedAuthorityDefaults("");//remove the ROLE_ prefix
}
}
注意:這裏說明一下hasRole(“ADMIN”)和hasAuthority(“ADMIN”)的區別,在鑑權的時候,hasRole會給 “ADMIN” 加上 ROLE_ 變成 “ROLE_ADMIN” 而hasAuthority則不會 還是 “ADMIN”、如果不想讓其添加前綴,可以使用如下代碼移除
//在上面也有體現
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults(){
return new GrantedAuthorityDefaults("");//remove the ROLE_ prefix
}
鑑權各種情況處理類
上述代碼引用的鑑權狀態處理代碼
無權訪問
/**
* @author: jamesluozhiwei
* @description: 無權訪問
*/
@Component
public class AjaxAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_AUTHORITY)));
}
}
用戶未登錄時返回給前端的數據
/**
* @author: jamesluozhiwei
* @description: 用戶未登錄時返回給前端的數據
*/
@Component
public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
request.setCharacterEncoding("utf-8");
response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_SIGN_IN)));
}
}
用戶登錄失敗時返回給前端的數據(本程序未使用)
適用於賬號密碼登錄模式
/**
* @author: jamesluozhiwei
* @description: 用戶登錄失敗時返回給前端的數據
*/
@Component
public class AjaxAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_CODE)));
}
}
用戶登錄成功時返回給前端的數據
適用於賬號密碼登錄模式
/**
* @author: jamesluozhiwei
* @description: 用戶登錄成功時返回給前端的數據
*/
@Component
@Slf4j
public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private RedisUtil redisUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//自定義login,不走這裏、若使用security的formLogin則自己添加業務實現(生成token、存儲token等等)
response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.NORMAL)));
}
}
登出成功
適用於賬號密碼登錄模式
/**
* @author: jamesluozhiwei
* @description: 登出成功
*/
@Component
@Slf4j
public class AjaxLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
private RedisUtil redisUtil;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//沒有logout不走這裏、若使用security的formLogin則自己添加業務實現(移除token等等)
response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.NORMAL)));
}
}
JWT自定義過濾器
在security配置類中有體現,主要用於解析token,並從redis中獲取用戶相關權限
import com.alibaba.fastjson.JSON;
import com.lzw.security.common.GenericResponse;
import com.lzw.security.common.ServiceError;
import com.lzw.security.entity.User;
import com.lzw.security.util.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Set;
/**
* @author: jamesluozhiwei
* @description: 確保在一次請求只通過一次filter,而不需要重複執行
*/
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Value("${token.expirationMilliSeconds}")
private long expirationMilliSeconds;
@Autowired
RedisUtil redisUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//獲取header中的token信息
String authHeader = request.getHeader("Authorization");
response.setCharacterEncoding("utf-8");
if (null == authHeader || !authHeader.startsWith("Bearer ")){
filterChain.doFilter(request,response);//token格式不正確
return;
}
String authToken = authHeader.substring("Bearer ".length());
String subject = JwtTokenUtil.parseToken(authToken);//獲取在token中自定義的subject,用作用戶標識,用來獲取用戶權限
//獲取redis中的token信息
if (!redisUtil.hasKey(authToken)){
//token 不存在 返回錯誤信息
response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_SIGN_IN)));
return;
}
//獲取緩存中的信息(根據自己的業務進行拓展)
HashMap<String,Object> hashMap = (HashMap<String, Object>) redisUtil.hget(authToken);
//從tokenInfo中取出用戶信息
User user = new User();
user.setId(Long.parseLong(hashMap.get("id").toString())).setAuthorities((Set<? extends GrantedAuthority>) hashMap.get("authorities"));
if (null == hashMap){
//用戶信息不存在或轉換錯誤,返回錯誤信息
response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_SIGN_IN)));
return;
}
//更新token過期時間
redisUtil.setKeyExpire(authToken,expirationMilliSeconds);
//將信息交給security
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request,response);
}
}
SelfUserDetailsService(基於自定義登錄,token驗證可忽略)
package com.lzw.security.service;
import com.lzw.security.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.HashSet;
import java.util.Set;
/**
* 用戶認證、權限、使用security的表單登錄時會被調用(自定義登錄請忽略)
* @author: jamesluozhiwei
*/
@Component
@Slf4j
public class SelfUserDetailsService implements UserDetailsService {
//@Autowired
//private UserMapper userMapper;
/**
* 若使用security表單鑑權則需實現該方法,通過username獲取用戶信息(密碼、權限等等)
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//通過username查詢用戶
//根據自己的業務獲取用戶信息
//SelfUserDetails user = userMapper.getUser(username);
//模擬從數據庫獲取到用戶信息
User user = new User();
if(user == null){
//仍需要細化處理
throw new UsernameNotFoundException("該用戶不存在");
}
Set authoritiesSet = new HashSet();
// 模擬從數據庫中獲取用戶權限
authoritiesSet.add(new SimpleGrantedAuthority("test:list"));
authoritiesSet.add(new SimpleGrantedAuthority("test:add"));
user.setAuthorities(authoritiesSet);
log.info("用戶{}驗證通過",username);
return user;
}
}
密碼加密方式
這裏就不用加密了
import org.springframework.security.crypto.password.PasswordEncoder;
public class NoPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return "";
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return true;
}
}
RBAC自定義鑑權
可在security配置中,通過
.and().authorizeRequests().anyRequest().access("@rbacauthorityservice.hasPermission(request,authentication)")//anyRequest表示全部
.and().authorizeRequests().antMatchers("/test/*").access("@rbacauthorityservice.hasPermission(request,authentication)")//也可以指定相應的地址
指定自定義鑑權方式,也可指定具體的URL
/**
* 鑑權處理
*/
@Component("rbacauthorityservice")//此處bean名稱要和上述的一致
public class RbacAuthorityService {
/**
* 可根據業務自定義鑑權
* @param request
* @param authentication 用戶權限信息
* @return 通過返回true 不通過則返回false(所有鑑權只要有一個通過了則爲通過)
*/
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
Object userInfo = authentication.getPrincipal();
boolean hasPermission = false;
if (userInfo instanceof UserDetails) {
String username = ((UserDetails) userInfo).getUsername();
//獲取資源
Set<String> urls = new HashSet();
// 這些 url 都是要登錄後才能訪問,且其他的 url 都不能訪問!
// 模擬鑑權(可根據自己的業務擴展)
urls.add("/demo/**");//application.yml裏設置了項目路徑,百度一下我就不貼了
Set set2 = new HashSet();
Set set3 = new HashSet();
AntPathMatcher antPathMatcher = new AntPathMatcher();
for (String url : urls) {
if (antPathMatcher.match(url, request.getRequestURI())) {
hasPermission = true;
break;
}
}
return hasPermission;
} else {
return false;
}
}
}
微信小程序相關
通過code換取openid
import com.alibaba.fastjson.JSONObject;
import com.lzw.security.common.WeChatUrl;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.codehaus.xfire.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.AlgorithmParameters;
import java.security.Security;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Slf4j
public class Jcode2SessionUtil {
/**
* 請求微信後臺獲取用戶數據
* @param code wx.login獲取到的臨時code
* @return 請求結果
* @throws Exception
*/
public static String jscode2session(String appid,String secret,String code,String grantType)throws Exception{
//定義返回的json對象
JSONObject result = new JSONObject();
//創建請求通過code換取session等數據
HttpPost httpPost = new HttpPost(WeChatUrl.JS_CODE_2_SESSION.getUrl());
List<NameValuePair> params=new ArrayList<NameValuePair>();
//建立一個NameValuePair數組,用於存儲欲傳送的參數
params.add(new BasicNameValuePair("appid",appid));
params.add(new BasicNameValuePair("secret",secret));
params.add(new BasicNameValuePair("js_code",code));
params.add(new BasicNameValuePair("grant_type",grantType));
//設置編碼
httpPost.setEntity(new UrlEncodedFormEntity(params));//添加參數
return EntityUtils.toString(new DefaultHttpClient().execute(httpPost).getEntity());
}
/**
* 解密用戶敏感數據獲取用戶信息
* @param sessionKey 數據進行加密簽名的密鑰
* @param encryptedData 包括敏感數據在內的完整用戶信息的加密數據
* @param iv 加密算法的初始向量
* @return
*/
public static String getUserInfo(String encryptedData,String sessionKey,String iv)throws Exception{
// 被加密的數據
byte[] dataByte = Base64.decode(encryptedData);
// 加密祕鑰
byte[] keyByte = Base64.decode(sessionKey);
// 偏移量
byte[] ivByte = Base64.decode(iv);
// 如果密鑰不足16位,那麼就補足. 這個if 中的內容很重要
int base = 16;
if (keyByte.length % base != 0) {
int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
byte[] temp = new byte[groups * base];
Arrays.fill(temp, (byte) 0);
System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
keyByte = temp;
}
// 初始化
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding","BC");
SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
parameters.init(new IvParameterSpec(ivByte));
cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化
byte[] resultByte = cipher.doFinal(dataByte);
if (null != resultByte && resultByte.length > 0) {
String result = new String(resultByte, "UTF-8");
log.info(result);
return result;
}
return null;
}
/**
* 獲取微信接口調用憑證
* @param appid
* @param secret
* @return 返回String 可轉JSON
*/
public static String getAccessToken(String appid,String secret){
JSONObject params = new JSONObject();
params.put("grant_type","client_credential");//獲取接口調用憑證
params.put("appid",appid);
params.put("secret",secret);
return HttpUtil.sendGet(WeChatUrl.GET_ACCESS_TOKEN.getUrl()+"?grant_type=client_credential&appid=" + appid + "&secret=" + secret);
}
/**
* 發送模板消息
* @param access_token 接口調用憑證
* @param touser 接收者(用戶)的 openid
* @param template_id 所需下發的模板消息id
* @param page 點擊模版卡片後跳轉的頁面,僅限本小程序內的頁面。支持帶參數,(eg:index?foo=bar)。該字段不填則模版無法跳轉
* @param form_id 表單提交場景下,爲submit事件帶上的formId;支付場景下,爲本次支付的 prepay_id
* @param data 模版內容,不填則下發空模版。具體格式請參照官網示例
* @param emphasis_keyword 模版需要放大的關鍵詞,不填則默認無放大
* @return 返回String可轉JSON
*/
public static String sendTemplateMessage(String access_token,String touser,String template_id,String page,String form_id,Object data,String emphasis_keyword){
JSONObject params = new JSONObject();
params.put("touser",touser);
params.put("template_id",template_id);
if (null != page && !"".equals(page)){
params.put("page",page);
}
params.put("form_id",form_id);
params.put("data",data);
if (null != emphasis_keyword && !"".equals(emphasis_keyword)){
params.put("emphasis_keyword",emphasis_keyword);
}
//發送請求
return HttpUtil.sendPost(WeChatUrl.SEND_TEMPLATE_MESSAGE.getUrl() + "?access_token=" + access_token,params.toString());
}
}
請求地址枚舉,可自行擴展
public enum WeChatUrl {
JS_CODE_2_SESSION("https://api.weixin.qq.com/sns/jscode2session")
,GET_ACCESS_TOKEN("https://api.weixin.qq.com/cgi-bin/token")
,SEND_TEMPLATE_MESSAGE("https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send")
;
private String url;
WeChatUrl() {
}
WeChatUrl(String url) {
this.url = url;
}
public String getUrl() {
return url;
}
public WeChatUrl setUrl(String url) {
this.url = url;
return this;
}
}
http工具類
import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Base64;
/**
* 請求工具類
* @author jamesluozhiwei
*/
public class HttpUtil {
/**
* 發送get請求
* @param url
* @return
*/
public static String sendGet(String url){
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet(url);
String result = null;
try {
HttpResponse response = httpClient.execute(httpGet);
HttpEntity entity = response.getEntity();
if (entity != null) {
result = EntityUtils.toString(entity, "UTF-8");
}
httpGet.releaseConnection();
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
/**
* 發送post請求
* @param url
* @param params 可使用JSONObject轉JSON字符串
* @return
*/
public static String sendPost(String url,String params){
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpPost httpPost = new HttpPost(url);
JSONObject jsonObject = null;
try {
httpPost.setEntity(new StringEntity(params, "UTF-8"));
HttpResponse response = httpClient.execute(httpPost);
return EntityUtils.toString(response.getEntity(),"UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 發送post請求
* @param httpUrl
* @param param JSON字符串
* @return
*/
public static String doPostBase64(String httpUrl, String param) {
HttpURLConnection connection = null;
InputStream is = null;
OutputStream os = null;
BufferedReader br = null;
String result = null;
try {
URL url = new URL(httpUrl);
// 通過遠程url連接對象打開連接
connection = (HttpURLConnection) url.openConnection();
// 設置連接請求方式
connection.setRequestMethod("POST");
// 設置連接主機服務器超時時間:15000毫秒
connection.setConnectTimeout(15000);
// 設置讀取主機服務器返回數據超時時間:60000毫秒
connection.setReadTimeout(60000);
// 默認值爲:false,當向遠程服務器傳送數據/寫數據時,需要設置爲true
connection.setDoOutput(true);
// 默認值爲:true,當前向遠程服務讀取數據時,設置爲true,該參數可有可無
connection.setDoInput(true);
// 設置傳入參數的格式:請求參數應該是 name1=value1&name2=value2 的形式。
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
// 通過連接對象獲取一個輸出流
os = connection.getOutputStream();
// 通過輸出流對象將參數寫出去/傳輸出去,它是通過字節數組寫出的
os.write(param.getBytes());
// 通過連接對象獲取一個輸入流,向遠程讀取
if (connection.getResponseCode() == 200) {
is = connection.getInputStream();
ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
byte[] buff = new byte[100];
int rc = 0;
while ((rc = is.read(buff, 0, 100)) > 0) {
swapStream.write(buff, 0, rc);
}
byte[] in2b = swapStream.toByteArray();
String tmp = new String(in2b);
if (tmp.indexOf("errcode") == -1)
return Base64.getEncoder().encodeToString(in2b);
return tmp;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 關閉資源
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != os) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 斷開與遠程地址url的連接
connection.disconnect();
}
return result;
}
}
業務層
/**
* 微信業務接口
*/
public interface WeChatService {
/**
* 小程序登錄
* @param code
* @return
*/
GenericResponse wxLogin(String code)throws Exception;
}
import com.alibaba.fastjson.JSONObject;
import com.lzw.security.common.GenericResponse;
import com.lzw.security.common.ServiceError;
import com.lzw.security.entity.User;
import com.lzw.security.service.WeChatService;
import com.lzw.security.util.Jcode2SessionUtil;
import com.lzw.security.util.JwtTokenUtil;
import com.lzw.security.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
/**
* 微信業務實現類
*/
@Service
@Slf4j
public class WeChatServiceImpl implements WeChatService {
@Value("${weChat.appid}")
private String appid;
@Value("${weChat.secret}")
private String secret;
@Autowired
private RedisUtil redisUtil;
@Override
public GenericResponse wxLogin(String code) throws Exception{
JSONObject sessionInfo = JSONObject.parseObject(jcode2Session(code));
Assert.notNull(sessionInfo,"code 無效");
Assert.isTrue(0 == sessionInfo.getInteger("errcode"),sessionInfo.getString("errmsg"));
// 獲取用戶唯一標識符 openid成功
// 模擬從數據庫獲取用戶信息
User user = new User();
user.setId(1L);
Set authoritiesSet = new HashSet();
// 模擬從數據庫中獲取用戶權限
authoritiesSet.add(new SimpleGrantedAuthority("test:add"));
authoritiesSet.add(new SimpleGrantedAuthority("test:list"));
authoritiesSet.add(new SimpleGrantedAuthority("ddd:list"));
user.setAuthorities(authoritiesSet);
HashMap<String,Object> hashMap = new HashMap<>();
hashMap.put("id",user.getId().toString());
hashMap.put("authorities",authoritiesSet);
String token = JwtTokenUtil.generateToken(user);
redisUtil.hset(token,hashMap);
return GenericResponse.response(ServiceError.NORMAL,token);
}
/**
* 登錄憑證校驗
* @param code
* @return
* @throws Exception
*/
private String jcode2Session(String code)throws Exception{
String sessionInfo = Jcode2SessionUtil.jscode2session(appid,secret,code,"authorization_code");//登錄grantType固定
log.info(sessionInfo);
return sessionInfo;
}
}
控制層
import com.lzw.security.common.GenericResponse;
import com.lzw.security.common.ServiceError;
import com.lzw.security.service.WeChatService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
private WeChatService weChatService;
/**
* code登錄獲取用戶openid
* @param code
* @return
* @throws Exception
*/
@PostMapping("/login")
public GenericResponse login(String code)throws Exception{
return weChatService.wxLogin(code);
}
/**
* 權限測試
*/
@GetMapping("/test")
public GenericResponse test(){
return GenericResponse.response(ServiceError.NORMAL,"test");
}
@PostMapping("/test")
public GenericResponse testPost(){
return GenericResponse.response(ServiceError.NORMAL,"testPOST");
}
@GetMapping("/test/a")
public GenericResponse testA(){
return GenericResponse.response(ServiceError.NORMAL,"testManage");
}
@GetMapping("/hello")
public GenericResponse hello(){
return GenericResponse.response(ServiceError.NORMAL,"hello security");
}
@GetMapping("/ddd")
@PreAuthorize("hasAuthority('ddd:list')")//基於表達式的權限驗證,調用此方法需有 "ddd:list" 的權限
public GenericResponse ddd(){
return GenericResponse.response(ServiceError.NORMAL,"dddList");
}
@PostMapping("/ddd")
@PreAuthorize("hasAuthority('ddd:add')")//基於表達式的權限驗證,調用此方法需有 "ddd:list" 的權限
public GenericResponse dddd(){
return GenericResponse.response(ServiceError.NORMAL,"testPOST");
}
}
工具類相關
redis
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* redis工具類
* @author: jamesluozhiwei
*/
@Component
public class RedisUtil {
@Value("${token.expirationMilliSeconds}")
private long expirationMilliSeconds;
//@Autowired
//private StringRedisTemplate redisTemplate;
@Autowired
private RedisTemplate redisTemplate;
/**
* 查詢key,支持模糊查詢
* @param key
* */
public Set<String> keys(String key){
return redisTemplate.keys(key);
}
/**
* 字符串獲取值
* @param key
* */
public Object get(String key){
return redisTemplate.opsForValue().get(key);
}
/**
* 字符串存入值
* 默認過期時間爲2小時
* @param key
* */
public void set(String key, String value){
set(key,value,expirationMilliSeconds);
}
/**
* 字符串存入值
* @param expire 過期時間(毫秒計)
* @param key
* */
public void set(String key, String value,long expire){
redisTemplate.opsForValue().set(key,value, expire,TimeUnit.MILLISECONDS);
}
/**
* 刪出key
* 這裏跟下邊deleteKey()最底層實現都是一樣的,應該可以通用
* @param key
* */
public void delete(String key){
redisTemplate.opsForValue().getOperations().delete(key);
}
/**
* 添加單個
* @param key key
* @param filed filed
* @param domain 對象
*/
public void hset(String key,String filed,Object domain){
hset(key,filed,domain,expirationMilliSeconds);
}
/**
* 添加單個
* @param key key
* @param filed filed
* @param domain 對象
* @param expire 過期時間(毫秒計)
*/
public void hset(String key,String filed,Object domain,long expire){
redisTemplate.opsForHash().put(key, filed, domain);
setKeyExpire(key,expirationMilliSeconds);
}
/**
* 添加HashMap
*
* @param key key
* @param hm 要存入的hash表
*/
public void hset(String key, HashMap<String,Object> hm){
redisTemplate.opsForHash().putAll(key,hm);
setKeyExpire(key,expirationMilliSeconds);
}
/**
* 如果key存在就不覆蓋
* @param key
* @param filed
* @param domain
*/
public void hsetAbsent(String key,String filed,Object domain){
redisTemplate.opsForHash().putIfAbsent(key, filed, domain);
}
/**
* 查詢key和field所確定的值
* @param key 查詢的key
* @param field 查詢的field
* @return HV
*/
public Object hget(String key,String field) {
return redisTemplate.opsForHash().get(key, field);
}
/**
* 查詢該key下所有值
* @param key 查詢的key
* @return Map<HK, HV>
*/
public Object hget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 刪除key下所有值
*
* @param key 查詢的key
*/
public void deleteKey(String key) {
redisTemplate.opsForHash().getOperations().delete(key);
}
/**
* 添加set集合
* @param key
* @param set
* @param expire
*/
public void sset(Object key,Set<?> set,long expire){
redisTemplate.opsForSet().add(key,set);
setKeyExpire(key,expire);
}
/**
* 添加set集合
* @param key
* @param set
*/
public void sset(Object key,Set<?> set){
sset(key, set,expirationMilliSeconds);
}
/**
* 判斷key和field下是否有值
* @param key 判斷的key
* @param field 判斷的field
*/
public Boolean hasKey(String key,String field) {
return redisTemplate.opsForHash().hasKey(key,field);
}
/**
* 判斷key下是否有值
* @param key 判斷的key
*/
public Boolean hasKey(String key) {
return redisTemplate.opsForHash().getOperations().hasKey(key);
}
/**
* 更新key的過期時間
* @param key
* @param expire
*/
public void setKeyExpire(Object key,long expire){
redisTemplate.expire(key,expire,TimeUnit.MILLISECONDS);
}
}
JWT生成解析工具
import com.lzw.security.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
/**
* @author: jamesluozhiwei
* @description: jwt生成token
*/
public class JwtTokenUtil {
private static final String SALT = "123456";//加密解密鹽值
/**
* 生成token(請根據自身業務擴展)
* @param subject (主體信息)
* @param expirationSeconds 過期時間(秒)
* @param claims 自定義身份信息
* @return
*/
public static String generateToken(String subject, int expirationSeconds, Map<String,Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)//主題
//.setExpiration(new Date(System.currentTimeMillis() + expirationSeconds * 1000))
.signWith(SignatureAlgorithm.HS512, SALT) // 不使用公鑰私鑰
//.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
}
/**
* 生成token
* @param user
* @return
*/
public static String generateToken(User user){
return Jwts.builder()
.setSubject(user.getId().toString())
.setExpiration(new Date(System.currentTimeMillis()))
.setIssuedAt(new Date())
.setIssuer("JAMES")
.signWith(SignatureAlgorithm.HS512, SALT)// 不使用公鑰私鑰
.compact();
}
/**
* 解析token,獲得subject中的信息
* @param token
* @return
*/
public static String parseToken(String token) {
String subject = null;
try {
subject = getTokenBody(token).getSubject();
} catch (Exception e) {
}
return subject;
}
/**
* 獲取token自定義屬性
* @param token
* @return
*/
public static Map<String,Object> getClaims(String token){
Map<String,Object> claims = null;
try {
claims = getTokenBody(token);
}catch (Exception e) {
}
return claims;
}
/**
* 解析token
* @param token
* @return
*/
private static Claims getTokenBody(String token){
return Jwts.parser()
//.setSigningKey(publicKey)
.setSigningKey(SALT)
.parseClaimsJws(token)
.getBody();
}
}
用戶實體
注意用戶實體需要實現 security 的 UserDetails
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Collection;
import java.util.Set;
public class User implements UserDetails, Serializable {
private Long id;
private String username;
private String password;
private Set<? extends GrantedAuthority> authorities;//權限列表
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@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 setUsername(String username) {
this.username = username;
return this;
}
public User setPassword(String password) {
this.password = password;
return this;
}
public User setAuthorities(Set<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
return this;
}
public Long getId() {
return id;
}
public User setId(Long id) {
this.id = id;
return this;
}
}
響應相關
public class GenericResponse {
private boolean success;
private int statusCode;
private Object content;
private String msg;
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public Object getContent() {
return content;
}
public void setContent(Object content) {
this.content = content;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public GenericResponse(){}
public GenericResponse(boolean success, int code, String msg, Object data) {
this.success = success;
this.statusCode = code;
this.msg = msg;
this.content = data;
}
public static GenericResponse response(ServiceError error) {
return GenericResponse.response(error, null);
}
public static GenericResponse response(ServiceError error, Object data) {
if (error == null) {
error = ServiceError.UN_KNOW_ERROR;
}
if (error.equals(ServiceError.NORMAL)) {
return GenericResponse.response(true, error.getCode(), error.getMsg(), data);
}
return GenericResponse.response(false, error.getCode(), error.getMsg(), data);
}
public static GenericResponse response(boolean success, int code, String msg, Object data) {
return new GenericResponse(success, code, msg, data);
}
}
public enum ServiceError {
NORMAL(1, "操作成功"),
UN_KNOW_ERROR(-1, "未知錯誤"),
/** Global Error */
GLOBAL_ERR_NO_SIGN_IN(-10001,"未登錄或登錄過期/Not sign in"),
GLOBAL_ERR_NO_CODE(-10002,"code錯誤/error code"),
GLOBAL_ERR_NO_AUTHORITY(-10003, "沒有操作權限/No operating rights"),
;
private int code;
private String msg;
private ServiceError(int code, String msg)
{
this.code=code;
this.msg=msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
springboot 啓動類
jar啓動請忽略,war啓動請繼承 SpringBootServletInitializer
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class SecurityApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
// war啓動請實現該方法
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(SecurityApplication.class);
}
}
postman演示
未登錄訪問接口
登錄後攜帶token訪問
項目地址
github地址:https://github.com/jamesluozhiwei/security
如果對您有幫助請高擡貴手點個star
個人博客:https://ccccyc.cn
關注公衆號獲取更多諮詢