├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── lzw │ │ └── security │ │ ├── SecurityApplication.java │ │ ├── common │ │ ├── GenericResponse.java │ │ ├── NoPasswordEncoder.java │ │ ├── ServiceError.java │ │ └── WeChatUrl.java │ │ ├── config │ │ ├── DruidConfiguration.java │ │ └── SpringSecurityConf.java │ │ ├── controller │ │ └── TestController.java │ │ ├── entity │ │ └── User.java │ │ ├── filter │ │ └── JwtAuthenticationTokenFilter.java │ │ ├── handler │ │ ├── AjaxAccessDeniedHandler.java │ │ ├── AjaxAuthenticationEntryPoint.java │ │ ├── AjaxAuthenticationFailureHandler.java │ │ ├── AjaxAuthenticationSuccessHandler.java │ │ └── AjaxLogoutSuccessHandler.java │ │ ├── service │ │ ├── RbacAuthorityService.java │ │ ├── SelfUserDetailsService.java │ │ ├── WeChatService.java │ │ └── impl │ │ │ └── WeChatServiceImpl.java │ │ └── util │ │ ├── AccessAddressUtil.java │ │ ├── CollectionUtil.java │ │ ├── DateUtil.java │ │ ├── HttpUtil.java │ │ ├── Jcode2SessionUtil.java │ │ ├── JwtTokenUtil.java │ │ └── RedisUtil.java ├── resources │ └── application.yml └── webapp │ └── WEB-INF │ └── web.xml └── test └── java └── com └── lzw └── security └── SecurityApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.io.FileOutputStream; 23 | import java.io.IOException; 24 | import java.net.URL; 25 | import java.nio.channels.Channels; 26 | import java.nio.channels.ReadableByteChannel; 27 | import java.util.Properties; 28 | 29 | public class MavenWrapperDownloader { 30 | 31 | /** 32 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 33 | */ 34 | private static final String DEFAULT_DOWNLOAD_URL = 35 | "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; 36 | 37 | /** 38 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 39 | * use instead of the default one. 40 | */ 41 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 42 | ".mvn/wrapper/maven-wrapper.properties"; 43 | 44 | /** 45 | * Path where the maven-wrapper.jar will be saved to. 46 | */ 47 | private static final String MAVEN_WRAPPER_JAR_PATH = 48 | ".mvn/wrapper/maven-wrapper.jar"; 49 | 50 | /** 51 | * Name of the property which should be used to override the default download url for the wrapper. 52 | */ 53 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 54 | 55 | public static void main(String args[]) { 56 | System.out.println("- Downloader started"); 57 | File baseDirectory = new File(args[0]); 58 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 59 | 60 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 61 | // wrapperUrl parameter. 62 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 63 | String url = DEFAULT_DOWNLOAD_URL; 64 | if (mavenWrapperPropertyFile.exists()) { 65 | FileInputStream mavenWrapperPropertyFileInputStream = null; 66 | try { 67 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 68 | Properties mavenWrapperProperties = new Properties(); 69 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 70 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 71 | } catch (IOException e) { 72 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 73 | } finally { 74 | try { 75 | if (mavenWrapperPropertyFileInputStream != null) { 76 | mavenWrapperPropertyFileInputStream.close(); 77 | } 78 | } catch (IOException e) { 79 | // Ignore ... 80 | } 81 | } 82 | } 83 | System.out.println("- Downloading from: : " + url); 84 | 85 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 86 | if (!outputFile.getParentFile().exists()) { 87 | if (!outputFile.getParentFile().mkdirs()) { 88 | System.out.println( 89 | "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 90 | } 91 | } 92 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 93 | try { 94 | downloadFileFromURL(url, outputFile); 95 | System.out.println("Done"); 96 | System.exit(0); 97 | } catch (Throwable e) { 98 | System.out.println("- Error downloading"); 99 | e.printStackTrace(); 100 | System.exit(1); 101 | } 102 | } 103 | 104 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 105 | URL website = new URL(urlString); 106 | ReadableByteChannel rbc; 107 | rbc = Channels.newChannel(website.openStream()); 108 | FileOutputStream fos = new FileOutputStream(destination); 109 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 110 | fos.close(); 111 | rbc.close(); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesluozhiwei/springboot-wechat-security/fdc21b8c95be4bda85efb59c6974b932ccaf183e/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # security 2 | springboot+security+jwt+redis 实现微信小程序登录及token权限鉴定 3 | tips:这是实战篇,默认各位看官具备相应的基础(文中使用了Lombok插件,如果使用源码请先安装插件) 4 | 5 | @[TOC] 6 | # 项目配置 7 | ## 依赖 8 | ```xml 9 | 10 | org.springframework.boot 11 | spring-boot-starter-data-redis 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-security 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter-web 20 | 21 | 22 | org.mybatis.spring.boot 23 | mybatis-spring-boot-starter 24 | 2.0.1 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-devtools 30 | runtime 31 | true 32 | 33 | 34 | mysql 35 | mysql-connector-java 36 | runtime 37 | 38 | 39 | org.projectlombok 40 | lombok 41 | true 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-test 46 | test 47 | 48 | 49 | org.springframework.security 50 | spring-security-test 51 | test 52 | 53 | 54 | 55 | io.jsonwebtoken 56 | jjwt 57 | 0.9.0 58 | 59 | 60 | 61 | com.alibaba 62 | fastjson 63 | 1.2.36 64 | 65 | 66 | 67 | com.alibaba 68 | druid 69 | 1.1.8 70 | 71 | 72 | 73 | log4j 74 | log4j 75 | 1.2.17 76 | 77 | 78 | 79 | 80 | org.apache.httpcomponents 81 | httpcore 82 | 4.4.11 83 | 84 | 85 | 86 | org.apache.httpcomponents 87 | httpclient 88 | 4.5.7 89 | 90 | 91 | 92 | 93 | org.bouncycastle 94 | bcprov-jdk15 95 | 1.46 96 | 97 | 98 | 99 | org.codehaus.xfire 100 | xfire-all 101 | 1.2.6 102 | 103 | 104 | org.springframework 105 | spring 106 | 107 | 108 | 109 | ``` 110 | ## application.yml 111 | ```yml 112 | spring: 113 | datasource: 114 | username: root 115 | password: 123456 116 | url: jdbc:mysql://localhost:3306/db_XXX?characterEncoding=utf-8&useSSl=false 117 | driver-class-name: com.mysql.jdbc.Driver 118 | # 此处使用Druid数据库连接池 119 | type: com.alibaba.druid.pool.DruidDataSource 120 | #监控统计拦截的filters 121 | filters: stat,wall,log4j 122 | #druid配置 123 | #配置初始化大小/最小/最大 124 | initialSize: 5 125 | minIdle: 5 126 | maxActive: 20 127 | #获取连接等待超时时间 128 | maxWait: 60000 129 | #间隔多久进行一次检测,检测需要关闭的空闲连接 130 | timeBetweenEvictionRunsMillis: 60000 131 | #一个连接在池中最小生存的时间 132 | minEvictableIdleTimeMillis: 300000 133 | validationQuery: SELECT 1 FROM DUAL 134 | testWhileIdle: true 135 | testOnBorrow: false 136 | testOnReturn: false 137 | #打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false 138 | poolPreparedStatements: false 139 | maxPoolPreparedStatementPerConnectionSize: 20 140 | # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 141 | connectionProperties: 142 | druid: 143 | stat: 144 | mergeSql: true 145 | slowSqlMillis: 5000 146 | http: 147 | encoding: 148 | charset: utf-8 149 | force: true 150 | enabled: true 151 | redis: 152 | host: 127.0.0.1 153 | port: 6379 154 | password: 123456 155 | 156 | 157 | #mybatis是独立节点,需要单独配置 158 | mybatis: 159 | mapper-locations: classpath*:mapper/*.xml 160 | type-aliases-package: com.lzw.security.entity 161 | configuration: 162 | map-underscore-to-camel-case: true 163 | 164 | server: 165 | port: 8080 166 | tomcat: 167 | uri-encoding: utf-8 168 | servlet: 169 | context-path: / 170 | 171 | #自定义参数,可以迁移走 172 | token: 173 | #redis默认过期时间(2小时)(这是自定义的)(毫秒) 174 | expirationMilliSeconds: 7200000 175 | 176 | #微信相关参数 177 | weChat: 178 | #小程序appid 179 | appid: aaaaaaaaaaaaaaaa 180 | #小程序密钥 181 | secret: ssssssssssssssss 182 | ``` 183 | 184 | # 程序代码 185 | ## security相关 186 | 187 | ### security核心配置类 188 | 189 | ```java 190 | import com.lzw.security.common.NoPasswordEncoder; 191 | import com.lzw.security.filter.JwtAuthenticationTokenFilter; 192 | import com.lzw.security.handler.*; 193 | import com.lzw.security.service.SelfUserDetailsService; 194 | import org.springframework.beans.factory.annotation.Autowired; 195 | import org.springframework.context.annotation.Bean; 196 | import org.springframework.context.annotation.Configuration; 197 | import org.springframework.http.HttpMethod; 198 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 199 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 200 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 201 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 202 | import org.springframework.security.config.core.GrantedAuthorityDefaults; 203 | import org.springframework.security.config.http.SessionCreationPolicy; 204 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 205 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 206 | 207 | /** 208 | * @author: jamesluozhiwei 209 | * @description: security核心配置类 210 | */ 211 | @Configuration 212 | @EnableGlobalMethodSecurity(prePostEnabled = true)//表示开启全局方法注解,可在指定方法上面添加注解指定权限,需含有指定权限才可调用(基于表达式的权限控制) 213 | public class SpringSecurityConf extends WebSecurityConfigurerAdapter { 214 | 215 | 216 | @Autowired 217 | AjaxAuthenticationEntryPoint authenticationEntryPoint;//未登陆时返回 JSON 格式的数据给前端(否则为 html) 218 | 219 | @Autowired 220 | AjaxAuthenticationSuccessHandler authenticationSuccessHandler; //登录成功返回的 JSON 格式数据给前端(否则为 html) 221 | 222 | @Autowired 223 | AjaxAuthenticationFailureHandler authenticationFailureHandler; //登录失败返回的 JSON 格式数据给前端(否则为 html) 224 | 225 | @Autowired 226 | AjaxLogoutSuccessHandler logoutSuccessHandler;//注销成功返回的 JSON 格式数据给前端(否则为 登录时的 html) 227 | 228 | @Autowired 229 | AjaxAccessDeniedHandler accessDeniedHandler;//无权访问返回的 JSON 格式数据给前端(否则为 403 html 页面) 230 | 231 | @Autowired 232 | SelfUserDetailsService userDetailsService; // 自定义user 233 | 234 | @Autowired 235 | JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // JWT 拦截器 236 | 237 | @Override 238 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 239 | // 加入自定义的安全认证 240 | //auth.authenticationProvider(provider); 241 | auth.userDetailsService(userDetailsService).passwordEncoder(new NoPasswordEncoder());//这里使用自定义的加密方式(不使用加密),security提供了 BCryptPasswordEncoder 加密可自定义或使用这个 242 | } 243 | 244 | @Override 245 | protected void configure(HttpSecurity http) throws Exception { 246 | // 请根据自身业务进行扩展 247 | // 去掉 CSRF 248 | http.csrf().disable() 249 | .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //关闭session管理,使用token机制处理 250 | .and() 251 | 252 | .httpBasic().authenticationEntryPoint(authenticationEntryPoint) 253 | //.and().antMatcher("/login") 254 | //.and().authorizeRequests().anyRequest().access("@rbacauthorityservice.hasPermission(request,authentication)")// 自定义权限校验 RBAC 动态 url 认证 255 | .and().authorizeRequests().antMatchers(HttpMethod.GET,"/test").hasAuthority("test:list") 256 | .and().authorizeRequests().antMatchers(HttpMethod.POST,"/test").hasAuthority("test:add") 257 | .and().authorizeRequests().antMatchers(HttpMethod.PUT,"/test").hasAuthority("test:update") 258 | .and().authorizeRequests().antMatchers(HttpMethod.DELETE,"/test").hasAuthority("test:delete") 259 | .and().authorizeRequests().antMatchers("/test/*").hasAuthority("test:manager") 260 | .and().authorizeRequests().antMatchers("/login").permitAll() //放行login(这里使用自定义登录) 261 | .and().authorizeRequests().antMatchers("/hello").permitAll();//permitAll表示不需要认证 262 | //微信小程序登录不给予账号密码,关闭 263 | // .and() 264 | //开启登录, 定义当需要用户登录时候,转到的登录页面、这是使用security提供的formLogin,不需要自己实现登录登出逻辑、但需要实现相关方法 265 | // .formLogin() 266 | // .loginPage("/test/login.html")//可不指定,使用security自带的登录页面 267 | // .loginProcessingUrl("/login") //登录地址 268 | // .successHandler(authenticationSuccessHandler) // 登录成功处理 269 | // .failureHandler(authenticationFailureHandler) // 登录失败处理 270 | // .permitAll() 271 | 272 | // .and() 273 | // .logout()//默认注销行为为logout 274 | // .logoutUrl("/logout") 275 | // .logoutSuccessHandler(logoutSuccessHandler) 276 | // .permitAll(); 277 | 278 | // 记住我 279 | // http.rememberMe().rememberMeParameter("remember-me") 280 | // .userDetailsService(userDetailsService).tokenValiditySeconds(1000); 281 | 282 | http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // 无权访问 JSON 格式的数据 283 | http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // JWT Filter 284 | 285 | } 286 | 287 | @Bean 288 | GrantedAuthorityDefaults grantedAuthorityDefaults(){ 289 | return new GrantedAuthorityDefaults("");//remove the ROLE_ prefix 290 | } 291 | 292 | } 293 | ``` 294 | 注意:这里说明一下hasRole("ADMIN")和hasAuthority("ADMIN")的区别,在鉴权的时候,hasRole会给 "ADMIN" 加上 ROLE_ 变成 "ROLE_ADMIN" 而hasAuthority则不会 还是 "ADMIN"、如果不想让其添加前缀,可以使用如下代码移除 295 | 296 | ```java 297 | //在上面也有体现 298 | @Bean 299 | GrantedAuthorityDefaults grantedAuthorityDefaults(){ 300 | return new GrantedAuthorityDefaults("");//remove the ROLE_ prefix 301 | } 302 | ``` 303 | ### 鉴权各种情况处理类 304 | 上述代码引用的鉴权状态处理代码 305 | 306 | #### 无权访问 307 | ```java 308 | /** 309 | * @author: jamesluozhiwei 310 | * @description: 无权访问 311 | */ 312 | @Component 313 | public class AjaxAccessDeniedHandler implements AccessDeniedHandler { 314 | @Override 315 | public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { 316 | response.setCharacterEncoding("utf-8"); 317 | response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_AUTHORITY))); 318 | } 319 | } 320 | ``` 321 | #### 用户未登录时返回给前端的数据 322 | ```java 323 | /** 324 | * @author: jamesluozhiwei 325 | * @description: 用户未登录时返回给前端的数据 326 | */ 327 | @Component 328 | public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint { 329 | 330 | @Override 331 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { 332 | request.setCharacterEncoding("utf-8"); 333 | response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_SIGN_IN))); 334 | } 335 | } 336 | ``` 337 | #### 用户登录失败时返回给前端的数据(本程序未使用) 338 | 适用于账号密码登录模式 339 | ```java 340 | /** 341 | * @author: jamesluozhiwei 342 | * @description: 用户登录失败时返回给前端的数据 343 | */ 344 | @Component 345 | public class AjaxAuthenticationFailureHandler implements AuthenticationFailureHandler { 346 | 347 | @Override 348 | public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { 349 | response.setCharacterEncoding("utf-8"); 350 | response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_CODE))); 351 | } 352 | 353 | } 354 | ``` 355 | #### 用户登录成功时返回给前端的数据 356 | 适用于账号密码登录模式 357 | ```java 358 | /** 359 | * @author: jamesluozhiwei 360 | * @description: 用户登录成功时返回给前端的数据 361 | */ 362 | @Component 363 | @Slf4j 364 | public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler { 365 | 366 | @Autowired 367 | private RedisUtil redisUtil; 368 | 369 | @Override 370 | public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { 371 | //自定义login,不走这里、若使用security的formLogin则自己添加业务实现(生成token、存储token等等) 372 | response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.NORMAL))); 373 | } 374 | } 375 | ``` 376 | #### 登出成功 377 | 适用于账号密码登录模式 378 | ```java 379 | /** 380 | * @author: jamesluozhiwei 381 | * @description: 登出成功 382 | */ 383 | @Component 384 | @Slf4j 385 | public class AjaxLogoutSuccessHandler implements LogoutSuccessHandler { 386 | 387 | @Autowired 388 | private RedisUtil redisUtil; 389 | 390 | @Override 391 | public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { 392 | //没有logout不走这里、若使用security的formLogin则自己添加业务实现(移除token等等) 393 | response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.NORMAL))); 394 | } 395 | 396 | } 397 | ``` 398 | 399 | ### JWT自定义过滤器 400 | 在security配置类中有体现,主要用于解析token,并从redis中获取用户相关权限 401 | ```java 402 | import com.alibaba.fastjson.JSON; 403 | import com.lzw.security.common.GenericResponse; 404 | import com.lzw.security.common.ServiceError; 405 | import com.lzw.security.entity.User; 406 | import com.lzw.security.util.*; 407 | import lombok.extern.slf4j.Slf4j; 408 | import org.springframework.beans.factory.annotation.Autowired; 409 | import org.springframework.beans.factory.annotation.Value; 410 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 411 | import org.springframework.security.core.GrantedAuthority; 412 | import org.springframework.security.core.context.SecurityContextHolder; 413 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 414 | import org.springframework.stereotype.Component; 415 | import org.springframework.web.filter.OncePerRequestFilter; 416 | 417 | import javax.servlet.FilterChain; 418 | import javax.servlet.ServletException; 419 | import javax.servlet.http.HttpServletRequest; 420 | import javax.servlet.http.HttpServletResponse; 421 | import java.io.IOException; 422 | import java.util.HashMap; 423 | import java.util.Set; 424 | 425 | /** 426 | * @author: jamesluozhiwei 427 | * @description: 确保在一次请求只通过一次filter,而不需要重复执行 428 | */ 429 | @Component 430 | @Slf4j 431 | public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { 432 | 433 | @Value("${token.expirationMilliSeconds}") 434 | private long expirationMilliSeconds; 435 | 436 | @Autowired 437 | RedisUtil redisUtil; 438 | 439 | @Override 440 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 441 | //获取header中的token信息 442 | String authHeader = request.getHeader("Authorization"); 443 | response.setCharacterEncoding("utf-8"); 444 | if (null == authHeader || !authHeader.startsWith("Bearer ")){ 445 | filterChain.doFilter(request,response);//token格式不正确 446 | return; 447 | } 448 | String authToken = authHeader.substring("Bearer ".length()); 449 | 450 | String subject = JwtTokenUtil.parseToken(authToken);//获取在token中自定义的subject,用作用户标识,用来获取用户权限 451 | 452 | //获取redis中的token信息 453 | 454 | if (!redisUtil.hasKey(authToken)){ 455 | //token 不存在 返回错误信息 456 | response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_SIGN_IN))); 457 | return; 458 | } 459 | 460 | //获取缓存中的信息(根据自己的业务进行拓展) 461 | HashMap hashMap = (HashMap) redisUtil.hget(authToken); 462 | //从tokenInfo中取出用户信息 463 | User user = new User(); 464 | user.setId(Long.parseLong(hashMap.get("id").toString())).setAuthorities((Set) hashMap.get("authorities")); 465 | if (null == hashMap){ 466 | //用户信息不存在或转换错误,返回错误信息 467 | response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_SIGN_IN))); 468 | return; 469 | } 470 | //更新token过期时间 471 | redisUtil.setKeyExpire(authToken,expirationMilliSeconds); 472 | //将信息交给security 473 | UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities()); 474 | authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 475 | SecurityContextHolder.getContext().setAuthentication(authenticationToken); 476 | filterChain.doFilter(request,response); 477 | } 478 | } 479 | ``` 480 | ### SelfUserDetailsService(基于自定义登录,token验证可忽略) 481 | ```java 482 | package com.lzw.security.service; 483 | import com.lzw.security.entity.User; 484 | import lombok.extern.slf4j.Slf4j; 485 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 486 | import org.springframework.security.core.userdetails.UserDetails; 487 | import org.springframework.security.core.userdetails.UserDetailsService; 488 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 489 | import org.springframework.stereotype.Component; 490 | 491 | import java.util.HashSet; 492 | import java.util.Set; 493 | 494 | /** 495 | * 用户认证、权限、使用security的表单登录时会被调用(自定义登录请忽略) 496 | * @author: jamesluozhiwei 497 | */ 498 | @Component 499 | @Slf4j 500 | public class SelfUserDetailsService implements UserDetailsService { 501 | 502 | //@Autowired 503 | //private UserMapper userMapper; 504 | 505 | /** 506 | * 若使用security表单鉴权则需实现该方法,通过username获取用户信息(密码、权限等等) 507 | * @param username 508 | * @return 509 | * @throws UsernameNotFoundException 510 | */ 511 | @Override 512 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 513 | 514 | //通过username查询用户 515 | //根据自己的业务获取用户信息 516 | //SelfUserDetails user = userMapper.getUser(username); 517 | //模拟从数据库获取到用户信息 518 | User user = new User(); 519 | if(user == null){ 520 | //仍需要细化处理 521 | throw new UsernameNotFoundException("该用户不存在"); 522 | } 523 | 524 | Set authoritiesSet = new HashSet(); 525 | // 模拟从数据库中获取用户权限 526 | authoritiesSet.add(new SimpleGrantedAuthority("test:list")); 527 | authoritiesSet.add(new SimpleGrantedAuthority("test:add")); 528 | user.setAuthorities(authoritiesSet); 529 | 530 | log.info("用户{}验证通过",username); 531 | return user; 532 | } 533 | } 534 | ``` 535 | ### 密码加密方式 536 | 这里就不用加密了 537 | ```java 538 | import org.springframework.security.crypto.password.PasswordEncoder; 539 | 540 | public class NoPasswordEncoder implements PasswordEncoder { 541 | @Override 542 | public String encode(CharSequence charSequence) { 543 | return ""; 544 | } 545 | 546 | @Override 547 | public boolean matches(CharSequence charSequence, String s) { 548 | return true; 549 | } 550 | } 551 | 552 | ``` 553 | ### RBAC自定义鉴权 554 | 可在security配置中,通过 555 | ```java 556 | .and().authorizeRequests().anyRequest().access("@rbacauthorityservice.hasPermission(request,authentication)")//anyRequest表示全部 557 | .and().authorizeRequests().antMatchers("/test/*").access("@rbacauthorityservice.hasPermission(request,authentication)")//也可以指定相应的地址 558 | ``` 559 | 指定自定义鉴权方式,也可指定具体的URL 560 | ```java 561 | /** 562 | * 鉴权处理 563 | */ 564 | @Component("rbacauthorityservice")//此处bean名称要和上述的一致 565 | public class RbacAuthorityService { 566 | /** 567 | * 可根据业务自定义鉴权 568 | * @param request 569 | * @param authentication 用户权限信息 570 | * @return 通过返回true 不通过则返回false(所有鉴权只要有一个通过了则为通过) 571 | */ 572 | public boolean hasPermission(HttpServletRequest request, Authentication authentication) { 573 | Object userInfo = authentication.getPrincipal(); 574 | 575 | boolean hasPermission = false; 576 | 577 | if (userInfo instanceof UserDetails) { 578 | 579 | String username = ((UserDetails) userInfo).getUsername(); 580 | 581 | //获取资源 582 | Set urls = new HashSet(); 583 | // 这些 url 都是要登录后才能访问,且其他的 url 都不能访问! 584 | // 模拟鉴权(可根据自己的业务扩展) 585 | urls.add("/demo/**");//application.yml里设置了项目路径,百度一下我就不贴了 586 | Set set2 = new HashSet(); 587 | Set set3 = new HashSet(); 588 | 589 | AntPathMatcher antPathMatcher = new AntPathMatcher(); 590 | 591 | for (String url : urls) { 592 | if (antPathMatcher.match(url, request.getRequestURI())) { 593 | hasPermission = true; 594 | break; 595 | } 596 | } 597 | return hasPermission; 598 | } else { 599 | return false; 600 | } 601 | } 602 | } 603 | ``` 604 | ## 微信小程序相关 605 | ### 通过code换取openid 606 | ```java 607 | import com.alibaba.fastjson.JSONObject; 608 | import com.lzw.security.common.WeChatUrl; 609 | import lombok.extern.slf4j.Slf4j; 610 | import org.apache.http.NameValuePair; 611 | import org.apache.http.client.entity.UrlEncodedFormEntity; 612 | import org.apache.http.client.methods.HttpPost; 613 | import org.apache.http.impl.client.DefaultHttpClient; 614 | import org.apache.http.message.BasicNameValuePair; 615 | import org.apache.http.util.EntityUtils; 616 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 617 | import org.codehaus.xfire.util.Base64; 618 | 619 | import javax.crypto.Cipher; 620 | import javax.crypto.spec.IvParameterSpec; 621 | import javax.crypto.spec.SecretKeySpec; 622 | import java.security.AlgorithmParameters; 623 | import java.security.Security; 624 | import java.util.ArrayList; 625 | import java.util.Arrays; 626 | import java.util.List; 627 | 628 | @Slf4j 629 | public class Jcode2SessionUtil { 630 | 631 | /** 632 | * 请求微信后台获取用户数据 633 | * @param code wx.login获取到的临时code 634 | * @return 请求结果 635 | * @throws Exception 636 | */ 637 | public static String jscode2session(String appid,String secret,String code,String grantType)throws Exception{ 638 | //定义返回的json对象 639 | JSONObject result = new JSONObject(); 640 | //创建请求通过code换取session等数据 641 | HttpPost httpPost = new HttpPost(WeChatUrl.JS_CODE_2_SESSION.getUrl()); 642 | List params=new ArrayList(); 643 | //建立一个NameValuePair数组,用于存储欲传送的参数 644 | params.add(new BasicNameValuePair("appid",appid)); 645 | params.add(new BasicNameValuePair("secret",secret)); 646 | params.add(new BasicNameValuePair("js_code",code)); 647 | params.add(new BasicNameValuePair("grant_type",grantType)); 648 | //设置编码 649 | httpPost.setEntity(new UrlEncodedFormEntity(params));//添加参数 650 | return EntityUtils.toString(new DefaultHttpClient().execute(httpPost).getEntity()); 651 | } 652 | /** 653 | * 解密用户敏感数据获取用户信息 654 | * @param sessionKey 数据进行加密签名的密钥 655 | * @param encryptedData 包括敏感数据在内的完整用户信息的加密数据 656 | * @param iv 加密算法的初始向量 657 | * @return 658 | */ 659 | public static String getUserInfo(String encryptedData,String sessionKey,String iv)throws Exception{ 660 | // 被加密的数据 661 | byte[] dataByte = Base64.decode(encryptedData); 662 | // 加密秘钥 663 | byte[] keyByte = Base64.decode(sessionKey); 664 | // 偏移量 665 | byte[] ivByte = Base64.decode(iv); 666 | // 如果密钥不足16位,那么就补足. 这个if 中的内容很重要 667 | int base = 16; 668 | if (keyByte.length % base != 0) { 669 | int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0); 670 | byte[] temp = new byte[groups * base]; 671 | Arrays.fill(temp, (byte) 0); 672 | System.arraycopy(keyByte, 0, temp, 0, keyByte.length); 673 | keyByte = temp; 674 | } 675 | // 初始化 676 | Security.addProvider(new BouncyCastleProvider()); 677 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding","BC"); 678 | SecretKeySpec spec = new SecretKeySpec(keyByte, "AES"); 679 | AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES"); 680 | parameters.init(new IvParameterSpec(ivByte)); 681 | cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化 682 | byte[] resultByte = cipher.doFinal(dataByte); 683 | if (null != resultByte && resultByte.length > 0) { 684 | String result = new String(resultByte, "UTF-8"); 685 | log.info(result); 686 | return result; 687 | } 688 | return null; 689 | } 690 | 691 | /** 692 | * 获取微信接口调用凭证 693 | * @param appid 694 | * @param secret 695 | * @return 返回String 可转JSON 696 | */ 697 | public static String getAccessToken(String appid,String secret){ 698 | JSONObject params = new JSONObject(); 699 | params.put("grant_type","client_credential");//获取接口调用凭证 700 | params.put("appid",appid); 701 | params.put("secret",secret); 702 | return HttpUtil.sendGet(WeChatUrl.GET_ACCESS_TOKEN.getUrl()+"?grant_type=client_credential&appid=" + appid + "&secret=" + secret); 703 | } 704 | 705 | /** 706 | * 发送模板消息 707 | * @param access_token 接口调用凭证 708 | * @param touser 接收者(用户)的 openid 709 | * @param template_id 所需下发的模板消息id 710 | * @param page 点击模版卡片后跳转的页面,仅限本小程序内的页面。支持带参数,(eg:index?foo=bar)。该字段不填则模版无法跳转 711 | * @param form_id 表单提交场景下,为submit事件带上的formId;支付场景下,为本次支付的 prepay_id 712 | * @param data 模版内容,不填则下发空模版。具体格式请参照官网示例 713 | * @param emphasis_keyword 模版需要放大的关键词,不填则默认无放大 714 | * @return 返回String可转JSON 715 | */ 716 | public static String sendTemplateMessage(String access_token,String touser,String template_id,String page,String form_id,Object data,String emphasis_keyword){ 717 | JSONObject params = new JSONObject(); 718 | params.put("touser",touser); 719 | params.put("template_id",template_id); 720 | if (null != page && !"".equals(page)){ 721 | params.put("page",page); 722 | } 723 | params.put("form_id",form_id); 724 | params.put("data",data); 725 | if (null != emphasis_keyword && !"".equals(emphasis_keyword)){ 726 | params.put("emphasis_keyword",emphasis_keyword); 727 | } 728 | //发送请求 729 | return HttpUtil.sendPost(WeChatUrl.SEND_TEMPLATE_MESSAGE.getUrl() + "?access_token=" + access_token,params.toString()); 730 | } 731 | 732 | } 733 | ``` 734 | 请求地址枚举,可自行扩展 735 | ```java 736 | public enum WeChatUrl { 737 | 738 | JS_CODE_2_SESSION("https://api.weixin.qq.com/sns/jscode2session") 739 | ,GET_ACCESS_TOKEN("https://api.weixin.qq.com/cgi-bin/token") 740 | ,SEND_TEMPLATE_MESSAGE("https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send") 741 | ; 742 | 743 | private String url; 744 | 745 | WeChatUrl() { 746 | } 747 | 748 | WeChatUrl(String url) { 749 | this.url = url; 750 | } 751 | 752 | public String getUrl() { 753 | return url; 754 | } 755 | 756 | public WeChatUrl setUrl(String url) { 757 | this.url = url; 758 | return this; 759 | } 760 | } 761 | ``` 762 | http工具类 763 | ```java 764 | import com.alibaba.fastjson.JSONObject; 765 | import org.apache.http.HttpEntity; 766 | import org.apache.http.HttpResponse; 767 | import org.apache.http.client.methods.HttpGet; 768 | import org.apache.http.client.methods.HttpPost; 769 | import org.apache.http.entity.StringEntity; 770 | import org.apache.http.impl.client.DefaultHttpClient; 771 | import org.apache.http.util.EntityUtils; 772 | import java.io.*; 773 | import java.net.HttpURLConnection; 774 | import java.net.URL; 775 | import java.util.Base64; 776 | /** 777 | * 请求工具类 778 | * @author jamesluozhiwei 779 | */ 780 | public class HttpUtil { 781 | 782 | /** 783 | * 发送get请求 784 | * @param url 785 | * @return 786 | */ 787 | public static String sendGet(String url){ 788 | DefaultHttpClient httpClient = new DefaultHttpClient(); 789 | HttpGet httpGet = new HttpGet(url); 790 | String result = null; 791 | try { 792 | HttpResponse response = httpClient.execute(httpGet); 793 | HttpEntity entity = response.getEntity(); 794 | if (entity != null) { 795 | result = EntityUtils.toString(entity, "UTF-8"); 796 | } 797 | httpGet.releaseConnection(); 798 | } catch (IOException e) { 799 | e.printStackTrace(); 800 | } 801 | return result; 802 | } 803 | 804 | 805 | /** 806 | * 发送post请求 807 | * @param url 808 | * @param params 可使用JSONObject转JSON字符串 809 | * @return 810 | */ 811 | public static String sendPost(String url,String params){ 812 | DefaultHttpClient httpClient = new DefaultHttpClient(); 813 | HttpPost httpPost = new HttpPost(url); 814 | JSONObject jsonObject = null; 815 | try { 816 | httpPost.setEntity(new StringEntity(params, "UTF-8")); 817 | HttpResponse response = httpClient.execute(httpPost); 818 | return EntityUtils.toString(response.getEntity(),"UTF-8"); 819 | } catch (IOException e) { 820 | e.printStackTrace(); 821 | } 822 | return null; 823 | } 824 | 825 | /** 826 | * 发送post请求 827 | * @param httpUrl 828 | * @param param JSON字符串 829 | * @return 830 | */ 831 | public static String doPostBase64(String httpUrl, String param) { 832 | 833 | HttpURLConnection connection = null; 834 | InputStream is = null; 835 | OutputStream os = null; 836 | BufferedReader br = null; 837 | String result = null; 838 | try { 839 | URL url = new URL(httpUrl); 840 | // 通过远程url连接对象打开连接 841 | connection = (HttpURLConnection) url.openConnection(); 842 | // 设置连接请求方式 843 | connection.setRequestMethod("POST"); 844 | // 设置连接主机服务器超时时间:15000毫秒 845 | connection.setConnectTimeout(15000); 846 | // 设置读取主机服务器返回数据超时时间:60000毫秒 847 | connection.setReadTimeout(60000); 848 | 849 | // 默认值为:false,当向远程服务器传送数据/写数据时,需要设置为true 850 | connection.setDoOutput(true); 851 | // 默认值为:true,当前向远程服务读取数据时,设置为true,该参数可有可无 852 | connection.setDoInput(true); 853 | // 设置传入参数的格式:请求参数应该是 name1=value1&name2=value2 的形式。 854 | connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); 855 | // 通过连接对象获取一个输出流 856 | os = connection.getOutputStream(); 857 | // 通过输出流对象将参数写出去/传输出去,它是通过字节数组写出的 858 | 859 | os.write(param.getBytes()); 860 | 861 | // 通过连接对象获取一个输入流,向远程读取 862 | if (connection.getResponseCode() == 200) { 863 | 864 | is = connection.getInputStream(); 865 | 866 | ByteArrayOutputStream swapStream = new ByteArrayOutputStream(); 867 | byte[] buff = new byte[100]; 868 | int rc = 0; 869 | while ((rc = is.read(buff, 0, 100)) > 0) { 870 | swapStream.write(buff, 0, rc); 871 | } 872 | byte[] in2b = swapStream.toByteArray(); 873 | String tmp = new String(in2b); 874 | if (tmp.indexOf("errcode") == -1) 875 | return Base64.getEncoder().encodeToString(in2b); 876 | return tmp; 877 | } 878 | } catch (Exception e) { 879 | e.printStackTrace(); 880 | } finally { 881 | // 关闭资源 882 | if (null != br) { 883 | try { 884 | br.close(); 885 | } catch (IOException e) { 886 | e.printStackTrace(); 887 | } 888 | } 889 | if (null != os) { 890 | try { 891 | os.close(); 892 | } catch (IOException e) { 893 | e.printStackTrace(); 894 | } 895 | } 896 | if (null != is) { 897 | try { 898 | is.close(); 899 | } catch (IOException e) { 900 | e.printStackTrace(); 901 | } 902 | } 903 | // 断开与远程地址url的连接 904 | connection.disconnect(); 905 | } 906 | return result; 907 | } 908 | } 909 | ``` 910 | ### 业务层 911 | ```java 912 | /** 913 | * 微信业务接口 914 | */ 915 | public interface WeChatService { 916 | 917 | /** 918 | * 小程序登录 919 | * @param code 920 | * @return 921 | */ 922 | GenericResponse wxLogin(String code)throws Exception; 923 | 924 | } 925 | ``` 926 | ```java 927 | import com.alibaba.fastjson.JSONObject; 928 | import com.lzw.security.common.GenericResponse; 929 | import com.lzw.security.common.ServiceError; 930 | import com.lzw.security.entity.User; 931 | import com.lzw.security.service.WeChatService; 932 | import com.lzw.security.util.Jcode2SessionUtil; 933 | import com.lzw.security.util.JwtTokenUtil; 934 | import com.lzw.security.util.RedisUtil; 935 | import lombok.extern.slf4j.Slf4j; 936 | import org.springframework.beans.factory.annotation.Autowired; 937 | import org.springframework.beans.factory.annotation.Value; 938 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 939 | import org.springframework.stereotype.Service; 940 | import org.springframework.util.Assert; 941 | import java.util.HashMap; 942 | import java.util.HashSet; 943 | import java.util.Set; 944 | 945 | /** 946 | * 微信业务实现类 947 | */ 948 | @Service 949 | @Slf4j 950 | public class WeChatServiceImpl implements WeChatService { 951 | 952 | @Value("${weChat.appid}") 953 | private String appid; 954 | 955 | @Value("${weChat.secret}") 956 | private String secret; 957 | 958 | @Autowired 959 | private RedisUtil redisUtil; 960 | 961 | @Override 962 | public GenericResponse wxLogin(String code) throws Exception{ 963 | JSONObject sessionInfo = JSONObject.parseObject(jcode2Session(code)); 964 | 965 | Assert.notNull(sessionInfo,"code 无效"); 966 | 967 | Assert.isTrue(0 == sessionInfo.getInteger("errcode"),sessionInfo.getString("errmsg")); 968 | 969 | // 获取用户唯一标识符 openid成功 970 | // 模拟从数据库获取用户信息 971 | User user = new User(); 972 | user.setId(1L); 973 | Set authoritiesSet = new HashSet(); 974 | // 模拟从数据库中获取用户权限 975 | authoritiesSet.add(new SimpleGrantedAuthority("test:add")); 976 | authoritiesSet.add(new SimpleGrantedAuthority("test:list")); 977 | authoritiesSet.add(new SimpleGrantedAuthority("ddd:list")); 978 | user.setAuthorities(authoritiesSet); 979 | HashMap hashMap = new HashMap<>(); 980 | hashMap.put("id",user.getId().toString()); 981 | hashMap.put("authorities",authoritiesSet); 982 | String token = JwtTokenUtil.generateToken(user); 983 | redisUtil.hset(token,hashMap); 984 | 985 | return GenericResponse.response(ServiceError.NORMAL,token); 986 | } 987 | 988 | /** 989 | * 登录凭证校验 990 | * @param code 991 | * @return 992 | * @throws Exception 993 | */ 994 | private String jcode2Session(String code)throws Exception{ 995 | String sessionInfo = Jcode2SessionUtil.jscode2session(appid,secret,code,"authorization_code");//登录grantType固定 996 | log.info(sessionInfo); 997 | return sessionInfo; 998 | } 999 | } 1000 | ``` 1001 | ### 控制层 1002 | ```java 1003 | import com.lzw.security.common.GenericResponse; 1004 | import com.lzw.security.common.ServiceError; 1005 | import com.lzw.security.service.WeChatService; 1006 | import org.springframework.beans.factory.annotation.Autowired; 1007 | import org.springframework.security.access.prepost.PreAuthorize; 1008 | import org.springframework.web.bind.annotation.GetMapping; 1009 | import org.springframework.web.bind.annotation.PostMapping; 1010 | import org.springframework.web.bind.annotation.RestController; 1011 | 1012 | @RestController 1013 | public class TestController { 1014 | 1015 | @Autowired 1016 | private WeChatService weChatService; 1017 | 1018 | /** 1019 | * code登录获取用户openid 1020 | * @param code 1021 | * @return 1022 | * @throws Exception 1023 | */ 1024 | @PostMapping("/login") 1025 | public GenericResponse login(String code)throws Exception{ 1026 | return weChatService.wxLogin(code); 1027 | } 1028 | 1029 | /** 1030 | * 权限测试 1031 | */ 1032 | 1033 | @GetMapping("/test") 1034 | public GenericResponse test(){ 1035 | return GenericResponse.response(ServiceError.NORMAL,"test"); 1036 | } 1037 | 1038 | @PostMapping("/test") 1039 | public GenericResponse testPost(){ 1040 | return GenericResponse.response(ServiceError.NORMAL,"testPOST"); 1041 | } 1042 | 1043 | @GetMapping("/test/a") 1044 | public GenericResponse testA(){ 1045 | return GenericResponse.response(ServiceError.NORMAL,"testManage"); 1046 | } 1047 | 1048 | @GetMapping("/hello") 1049 | public GenericResponse hello(){ 1050 | return GenericResponse.response(ServiceError.NORMAL,"hello security"); 1051 | } 1052 | 1053 | @GetMapping("/ddd") 1054 | @PreAuthorize("hasAuthority('ddd:list')")//基于表达式的权限验证,调用此方法需有 "ddd:list" 的权限 1055 | public GenericResponse ddd(){ 1056 | return GenericResponse.response(ServiceError.NORMAL,"dddList"); 1057 | } 1058 | 1059 | @PostMapping("/ddd") 1060 | @PreAuthorize("hasAuthority('ddd:add')")//基于表达式的权限验证,调用此方法需有 "ddd:list" 的权限 1061 | public GenericResponse dddd(){ 1062 | return GenericResponse.response(ServiceError.NORMAL,"testPOST"); 1063 | } 1064 | } 1065 | ``` 1066 | ## 工具类相关 1067 | ### redis 1068 | ```java 1069 | import org.springframework.beans.factory.annotation.Autowired; 1070 | import org.springframework.beans.factory.annotation.Value; 1071 | import org.springframework.data.redis.core.RedisTemplate; 1072 | import org.springframework.stereotype.Component; 1073 | import java.util.HashMap; 1074 | import java.util.Set; 1075 | import java.util.concurrent.TimeUnit; 1076 | /** 1077 | * redis工具类 1078 | * @author: jamesluozhiwei 1079 | */ 1080 | @Component 1081 | public class RedisUtil { 1082 | 1083 | @Value("${token.expirationMilliSeconds}") 1084 | private long expirationMilliSeconds; 1085 | 1086 | //@Autowired 1087 | //private StringRedisTemplate redisTemplate; 1088 | 1089 | @Autowired 1090 | private RedisTemplate redisTemplate; 1091 | 1092 | /** 1093 | * 查询key,支持模糊查询 1094 | * @param key 1095 | * */ 1096 | public Set keys(String key){ 1097 | return redisTemplate.keys(key); 1098 | } 1099 | 1100 | /** 1101 | * 字符串获取值 1102 | * @param key 1103 | * */ 1104 | public Object get(String key){ 1105 | return redisTemplate.opsForValue().get(key); 1106 | } 1107 | 1108 | /** 1109 | * 字符串存入值 1110 | * 默认过期时间为2小时 1111 | * @param key 1112 | * */ 1113 | public void set(String key, String value){ 1114 | set(key,value,expirationMilliSeconds); 1115 | } 1116 | 1117 | /** 1118 | * 字符串存入值 1119 | * @param expire 过期时间(毫秒计) 1120 | * @param key 1121 | * */ 1122 | public void set(String key, String value,long expire){ 1123 | redisTemplate.opsForValue().set(key,value, expire,TimeUnit.MILLISECONDS); 1124 | } 1125 | 1126 | /** 1127 | * 删出key 1128 | * 这里跟下边deleteKey()最底层实现都是一样的,应该可以通用 1129 | * @param key 1130 | * */ 1131 | public void delete(String key){ 1132 | redisTemplate.opsForValue().getOperations().delete(key); 1133 | } 1134 | 1135 | /** 1136 | * 添加单个 1137 | * @param key key 1138 | * @param filed filed 1139 | * @param domain 对象 1140 | */ 1141 | public void hset(String key,String filed,Object domain){ 1142 | hset(key,filed,domain,expirationMilliSeconds); 1143 | } 1144 | 1145 | /** 1146 | * 添加单个 1147 | * @param key key 1148 | * @param filed filed 1149 | * @param domain 对象 1150 | * @param expire 过期时间(毫秒计) 1151 | */ 1152 | public void hset(String key,String filed,Object domain,long expire){ 1153 | redisTemplate.opsForHash().put(key, filed, domain); 1154 | setKeyExpire(key,expirationMilliSeconds); 1155 | } 1156 | 1157 | /** 1158 | * 添加HashMap 1159 | * 1160 | * @param key key 1161 | * @param hm 要存入的hash表 1162 | */ 1163 | public void hset(String key, HashMap hm){ 1164 | redisTemplate.opsForHash().putAll(key,hm); 1165 | setKeyExpire(key,expirationMilliSeconds); 1166 | } 1167 | 1168 | /** 1169 | * 如果key存在就不覆盖 1170 | * @param key 1171 | * @param filed 1172 | * @param domain 1173 | */ 1174 | public void hsetAbsent(String key,String filed,Object domain){ 1175 | redisTemplate.opsForHash().putIfAbsent(key, filed, domain); 1176 | } 1177 | 1178 | /** 1179 | * 查询key和field所确定的值 1180 | * @param key 查询的key 1181 | * @param field 查询的field 1182 | * @return HV 1183 | */ 1184 | public Object hget(String key,String field) { 1185 | return redisTemplate.opsForHash().get(key, field); 1186 | } 1187 | 1188 | /** 1189 | * 查询该key下所有值 1190 | * @param key 查询的key 1191 | * @return Map 1192 | */ 1193 | public Object hget(String key) { 1194 | return redisTemplate.opsForHash().entries(key); 1195 | } 1196 | 1197 | /** 1198 | * 删除key下所有值 1199 | * 1200 | * @param key 查询的key 1201 | */ 1202 | public void deleteKey(String key) { 1203 | redisTemplate.opsForHash().getOperations().delete(key); 1204 | } 1205 | 1206 | /** 1207 | * 添加set集合 1208 | * @param key 1209 | * @param set 1210 | * @param expire 1211 | */ 1212 | public void sset(Object key,Set set,long expire){ 1213 | redisTemplate.opsForSet().add(key,set); 1214 | setKeyExpire(key,expire); 1215 | } 1216 | 1217 | /** 1218 | * 添加set集合 1219 | * @param key 1220 | * @param set 1221 | */ 1222 | public void sset(Object key,Set set){ 1223 | sset(key, set,expirationMilliSeconds); 1224 | } 1225 | 1226 | /** 1227 | * 判断key和field下是否有值 1228 | * @param key 判断的key 1229 | * @param field 判断的field 1230 | */ 1231 | public Boolean hasKey(String key,String field) { 1232 | return redisTemplate.opsForHash().hasKey(key,field); 1233 | } 1234 | 1235 | /** 1236 | * 判断key下是否有值 1237 | * @param key 判断的key 1238 | */ 1239 | public Boolean hasKey(String key) { 1240 | return redisTemplate.opsForHash().getOperations().hasKey(key); 1241 | } 1242 | 1243 | /** 1244 | * 更新key的过期时间 1245 | * @param key 1246 | * @param expire 1247 | */ 1248 | public void setKeyExpire(Object key,long expire){ 1249 | redisTemplate.expire(key,expire,TimeUnit.MILLISECONDS); 1250 | } 1251 | } 1252 | ``` 1253 | ### JWT生成解析工具 1254 | ```java 1255 | import com.lzw.security.entity.User; 1256 | import io.jsonwebtoken.Claims; 1257 | import io.jsonwebtoken.Jwts; 1258 | import io.jsonwebtoken.SignatureAlgorithm; 1259 | import java.util.Date; 1260 | import java.util.Map; 1261 | /** 1262 | * @author: jamesluozhiwei 1263 | * @description: jwt生成token 1264 | */ 1265 | public class JwtTokenUtil { 1266 | 1267 | private static final String SALT = "123456";//加密解密盐值 1268 | 1269 | /** 1270 | * 生成token(请根据自身业务扩展) 1271 | * @param subject (主体信息) 1272 | * @param expirationSeconds 过期时间(秒) 1273 | * @param claims 自定义身份信息 1274 | * @return 1275 | */ 1276 | public static String generateToken(String subject, int expirationSeconds, Map claims) { 1277 | return Jwts.builder() 1278 | .setClaims(claims) 1279 | .setSubject(subject)//主题 1280 | //.setExpiration(new Date(System.currentTimeMillis() + expirationSeconds * 1000)) 1281 | .signWith(SignatureAlgorithm.HS512, SALT) // 不使用公钥私钥 1282 | //.signWith(SignatureAlgorithm.RS256, privateKey) 1283 | .compact(); 1284 | } 1285 | 1286 | /** 1287 | * 生成token 1288 | * @param user 1289 | * @return 1290 | */ 1291 | public static String generateToken(User user){ 1292 | return Jwts.builder() 1293 | .setSubject(user.getId().toString()) 1294 | .setExpiration(new Date(System.currentTimeMillis())) 1295 | .setIssuedAt(new Date()) 1296 | .setIssuer("JAMES") 1297 | .signWith(SignatureAlgorithm.HS512, SALT)// 不使用公钥私钥 1298 | .compact(); 1299 | } 1300 | 1301 | /** 1302 | * 解析token,获得subject中的信息 1303 | * @param token 1304 | * @return 1305 | */ 1306 | public static String parseToken(String token) { 1307 | String subject = null; 1308 | try { 1309 | subject = getTokenBody(token).getSubject(); 1310 | } catch (Exception e) { 1311 | } 1312 | return subject; 1313 | } 1314 | 1315 | /** 1316 | * 获取token自定义属性 1317 | * @param token 1318 | * @return 1319 | */ 1320 | public static Map getClaims(String token){ 1321 | Map claims = null; 1322 | try { 1323 | claims = getTokenBody(token); 1324 | }catch (Exception e) { 1325 | } 1326 | 1327 | return claims; 1328 | } 1329 | 1330 | /** 1331 | * 解析token 1332 | * @param token 1333 | * @return 1334 | */ 1335 | private static Claims getTokenBody(String token){ 1336 | return Jwts.parser() 1337 | //.setSigningKey(publicKey) 1338 | .setSigningKey(SALT) 1339 | .parseClaimsJws(token) 1340 | .getBody(); 1341 | } 1342 | } 1343 | ``` 1344 | ### 用户实体 1345 | 注意用户实体需要实现 security 的 UserDetails 1346 | ```java 1347 | import org.springframework.security.core.GrantedAuthority; 1348 | import org.springframework.security.core.userdetails.UserDetails; 1349 | 1350 | import java.io.Serializable; 1351 | import java.util.Collection; 1352 | import java.util.Set; 1353 | 1354 | public class User implements UserDetails, Serializable { 1355 | 1356 | private Long id; 1357 | 1358 | private String username; 1359 | 1360 | private String password; 1361 | 1362 | private Set authorities;//权限列表 1363 | 1364 | @Override 1365 | public Collection getAuthorities() { 1366 | return this.authorities; 1367 | } 1368 | 1369 | @Override 1370 | public String getPassword() { 1371 | return this.password; 1372 | } 1373 | 1374 | @Override 1375 | public String getUsername() { 1376 | return this.username; 1377 | } 1378 | 1379 | @Override 1380 | public boolean isAccountNonExpired() { 1381 | return true; 1382 | } 1383 | 1384 | @Override 1385 | public boolean isAccountNonLocked() { 1386 | return true; 1387 | } 1388 | 1389 | @Override 1390 | public boolean isCredentialsNonExpired() { 1391 | return true; 1392 | } 1393 | 1394 | @Override 1395 | public boolean isEnabled() { 1396 | return true; 1397 | } 1398 | 1399 | public User setUsername(String username) { 1400 | this.username = username; 1401 | return this; 1402 | } 1403 | 1404 | public User setPassword(String password) { 1405 | this.password = password; 1406 | return this; 1407 | } 1408 | 1409 | public User setAuthorities(Set authorities) { 1410 | this.authorities = authorities; 1411 | return this; 1412 | } 1413 | 1414 | public Long getId() { 1415 | return id; 1416 | } 1417 | 1418 | public User setId(Long id) { 1419 | this.id = id; 1420 | return this; 1421 | } 1422 | } 1423 | ``` 1424 | ### 响应相关 1425 | ```java 1426 | public class GenericResponse { 1427 | 1428 | private boolean success; 1429 | private int statusCode; 1430 | private Object content; 1431 | private String msg; 1432 | 1433 | public boolean isSuccess() { 1434 | return success; 1435 | } 1436 | 1437 | public void setSuccess(boolean success) { 1438 | this.success = success; 1439 | } 1440 | 1441 | public int getStatusCode() { 1442 | return statusCode; 1443 | } 1444 | 1445 | public void setStatusCode(int statusCode) { 1446 | this.statusCode = statusCode; 1447 | } 1448 | 1449 | public Object getContent() { 1450 | return content; 1451 | } 1452 | 1453 | public void setContent(Object content) { 1454 | this.content = content; 1455 | } 1456 | 1457 | public String getMsg() { 1458 | return msg; 1459 | } 1460 | 1461 | public void setMsg(String msg) { 1462 | this.msg = msg; 1463 | } 1464 | 1465 | public GenericResponse(){} 1466 | 1467 | public GenericResponse(boolean success, int code, String msg, Object data) { 1468 | 1469 | this.success = success; 1470 | this.statusCode = code; 1471 | this.msg = msg; 1472 | this.content = data; 1473 | } 1474 | 1475 | public static GenericResponse response(ServiceError error) { 1476 | 1477 | return GenericResponse.response(error, null); 1478 | } 1479 | 1480 | public static GenericResponse response(ServiceError error, Object data) { 1481 | 1482 | if (error == null) { 1483 | error = ServiceError.UN_KNOW_ERROR; 1484 | } 1485 | if (error.equals(ServiceError.NORMAL)) { 1486 | return GenericResponse.response(true, error.getCode(), error.getMsg(), data); 1487 | } 1488 | return GenericResponse.response(false, error.getCode(), error.getMsg(), data); 1489 | } 1490 | 1491 | public static GenericResponse response(boolean success, int code, String msg, Object data) { 1492 | 1493 | return new GenericResponse(success, code, msg, data); 1494 | } 1495 | } 1496 | ``` 1497 | ```java 1498 | public enum ServiceError { 1499 | 1500 | NORMAL(1, "操作成功"), 1501 | UN_KNOW_ERROR(-1, "未知错误"), 1502 | 1503 | /** Global Error */ 1504 | GLOBAL_ERR_NO_SIGN_IN(-10001,"未登录或登录过期/Not sign in"), 1505 | GLOBAL_ERR_NO_CODE(-10002,"code错误/error code"), 1506 | GLOBAL_ERR_NO_AUTHORITY(-10003, "没有操作权限/No operating rights"), 1507 | ; 1508 | 1509 | private int code; 1510 | private String msg; 1511 | 1512 | private ServiceError(int code, String msg) 1513 | { 1514 | this.code=code; 1515 | this.msg=msg; 1516 | } 1517 | 1518 | public int getCode() { 1519 | return code; 1520 | } 1521 | 1522 | public String getMsg() { 1523 | return msg; 1524 | } 1525 | } 1526 | ``` 1527 | ## springboot 启动类 1528 | jar启动请忽略,war启动请继承 SpringBootServletInitializer 1529 | ```java 1530 | import org.springframework.boot.SpringApplication; 1531 | import org.springframework.boot.autoconfigure.SpringBootApplication; 1532 | import org.springframework.boot.builder.SpringApplicationBuilder; 1533 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 1534 | 1535 | @SpringBootApplication 1536 | public class SecurityApplication extends SpringBootServletInitializer { 1537 | 1538 | public static void main(String[] args) { 1539 | SpringApplication.run(SecurityApplication.class, args); 1540 | } 1541 | 1542 | // war启动请实现该方法 1543 | @Override 1544 | protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { 1545 | return builder.sources(SecurityApplication.class); 1546 | } 1547 | } 1548 | ``` 1549 | # postman演示 1550 | 未登录访问接口 1551 | ![未登录访问接口](https://raw.githubusercontent.com/jamesluozhiwei/FigureBed/master/2019/springboot/security-not-login.png) 1552 | 1553 | 登录后携带token访问 1554 | ![登录后携带token访问](https://raw.githubusercontent.com/jamesluozhiwei/FigureBed/master/2019/springboot/security-logined.png) 1555 | # 项目地址 1556 | github地址:[https://github.com/jamesluozhiwei/security](https://github.com/jamesluozhiwei/security) 1557 | 1558 | 如果对您有帮助请高抬贵手点个star 1559 | 1560 | --- 1561 | 个人博客:[https://ccccyc.cn](https://ccccyc.cn) 1562 | 1563 | 关注公众号获取更多咨询 1564 | 1565 | ![Java开发小驿站](https://www.github.com/jamesluozhiwei/FigureBed/raw/master/2019/6/22/qrcode_for_gh_5926b81f45c6_258.jpg) 1566 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | ########################################################################################## 204 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 205 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 206 | ########################################################################################## 207 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found .mvn/wrapper/maven-wrapper.jar" 210 | fi 211 | else 212 | if [ "$MVNW_VERBOSE" = true ]; then 213 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 214 | fi 215 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 216 | while IFS="=" read key value; do 217 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 218 | esac 219 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Downloading from: $jarUrl" 222 | fi 223 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 224 | 225 | if command -v wget > /dev/null; then 226 | if [ "$MVNW_VERBOSE" = true ]; then 227 | echo "Found wget ... using wget" 228 | fi 229 | wget "$jarUrl" -O "$wrapperJarPath" 230 | elif command -v curl > /dev/null; then 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Found curl ... using curl" 233 | fi 234 | curl -o "$wrapperJarPath" "$jarUrl" 235 | else 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Falling back to using Java to download" 238 | fi 239 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 240 | if [ -e "$javaClass" ]; then 241 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 242 | if [ "$MVNW_VERBOSE" = true ]; then 243 | echo " - Compiling MavenWrapperDownloader.java ..." 244 | fi 245 | # Compiling the Java class 246 | ("$JAVA_HOME/bin/javac" "$javaClass") 247 | fi 248 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 249 | # Running the downloader 250 | if [ "$MVNW_VERBOSE" = true ]; then 251 | echo " - Running MavenWrapperDownloader.java ..." 252 | fi 253 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 254 | fi 255 | fi 256 | fi 257 | fi 258 | ########################################################################################## 259 | # End of extension 260 | ########################################################################################## 261 | 262 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 263 | if [ "$MVNW_VERBOSE" = true ]; then 264 | echo $MAVEN_PROJECTBASEDIR 265 | fi 266 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 267 | 268 | # For Cygwin, switch paths to Windows format before running java 269 | if $cygwin; then 270 | [ -n "$M2_HOME" ] && 271 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 272 | [ -n "$JAVA_HOME" ] && 273 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 274 | [ -n "$CLASSPATH" ] && 275 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 276 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 277 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 278 | fi 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 285 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 286 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 287 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 124 | FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( 125 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | echo Found %WRAPPER_JAR% 132 | ) else ( 133 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 134 | echo Downloading from: %DOWNLOAD_URL% 135 | powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" 136 | echo Finished downloading %WRAPPER_JAR% 137 | ) 138 | @REM End of extension 139 | 140 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 141 | if ERRORLEVEL 1 goto error 142 | goto end 143 | 144 | :error 145 | set ERROR_CODE=1 146 | 147 | :end 148 | @endlocal & set ERROR_CODE=%ERROR_CODE% 149 | 150 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 151 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 152 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 153 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 154 | :skipRcPost 155 | 156 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 157 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 158 | 159 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 160 | 161 | exit /B %ERROR_CODE% 162 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.6.RELEASE 9 | 10 | 11 | com.lzw 12 | security 13 | 0.0.1-SNAPSHOT 14 | security 15 | war 16 | Security project for Spring Boot 17 | 18 | 19 | 1.8 20 | 1.1.8 21 | 1.2.36 22 | 0.9.0 23 | 24 | 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-data-redis 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-security 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-web 37 | 38 | 39 | org.mybatis.spring.boot 40 | mybatis-spring-boot-starter 41 | 2.0.1 42 | 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-devtools 47 | runtime 48 | true 49 | 50 | 51 | mysql 52 | mysql-connector-java 53 | runtime 54 | 55 | 56 | org.projectlombok 57 | lombok 58 | true 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-starter-test 63 | test 64 | 65 | 66 | org.springframework.security 67 | spring-security-test 68 | test 69 | 70 | 71 | 72 | io.jsonwebtoken 73 | jjwt 74 | ${jjwt.version} 75 | 76 | 77 | 78 | com.alibaba 79 | fastjson 80 | ${fastjson.version} 81 | 82 | 83 | 84 | com.alibaba 85 | druid 86 | 1.1.8 87 | 88 | 89 | 90 | log4j 91 | log4j 92 | 1.2.17 93 | 94 | 95 | 96 | 97 | org.apache.httpcomponents 98 | httpcore 99 | 4.4.11 100 | 101 | 102 | 103 | org.apache.httpcomponents 104 | httpclient 105 | 4.5.7 106 | 107 | 108 | 109 | 110 | org.bouncycastle 111 | bcprov-jdk15 112 | 1.46 113 | 114 | 115 | 116 | org.codehaus.xfire 117 | xfire-all 118 | 1.2.6 119 | 120 | 121 | org.springframework 122 | spring 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | org.springframework.boot 132 | spring-boot-maven-plugin 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/SecurityApplication.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.builder.SpringApplicationBuilder; 6 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 7 | 8 | @SpringBootApplication 9 | public class SecurityApplication extends SpringBootServletInitializer { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(SecurityApplication.class, args); 13 | } 14 | 15 | @Override 16 | protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { 17 | return builder.sources(SecurityApplication.class); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/common/GenericResponse.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.common; 2 | 3 | public class GenericResponse { 4 | 5 | private boolean success; 6 | private int statusCode; 7 | private Object content; 8 | private String msg; 9 | 10 | public boolean isSuccess() { 11 | return success; 12 | } 13 | 14 | public void setSuccess(boolean success) { 15 | this.success = success; 16 | } 17 | 18 | public int getStatusCode() { 19 | return statusCode; 20 | } 21 | 22 | public void setStatusCode(int statusCode) { 23 | this.statusCode = statusCode; 24 | } 25 | 26 | public Object getContent() { 27 | return content; 28 | } 29 | 30 | public void setContent(Object content) { 31 | this.content = content; 32 | } 33 | 34 | public String getMsg() { 35 | return msg; 36 | } 37 | 38 | public void setMsg(String msg) { 39 | this.msg = msg; 40 | } 41 | 42 | public GenericResponse(){} 43 | 44 | public GenericResponse(boolean success, int code, String msg, Object data) { 45 | 46 | this.success = success; 47 | this.statusCode = code; 48 | this.msg = msg; 49 | this.content = data; 50 | } 51 | 52 | public static GenericResponse response(ServiceError error) { 53 | 54 | return GenericResponse.response(error, null); 55 | } 56 | 57 | public static GenericResponse response(ServiceError error, Object data) { 58 | 59 | if (error == null) { 60 | error = ServiceError.UN_KNOW_ERROR; 61 | } 62 | if (error.equals(ServiceError.NORMAL)) { 63 | return GenericResponse.response(true, error.getCode(), error.getMsg(), data); 64 | } 65 | return GenericResponse.response(false, error.getCode(), error.getMsg(), data); 66 | } 67 | 68 | public static GenericResponse response(boolean success, int code, String msg, Object data) { 69 | 70 | return new GenericResponse(success, code, msg, data); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/common/NoPasswordEncoder.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.common; 2 | 3 | import org.springframework.security.crypto.password.PasswordEncoder; 4 | 5 | public class NoPasswordEncoder implements PasswordEncoder { 6 | @Override 7 | public String encode(CharSequence charSequence) { 8 | return ""; 9 | } 10 | 11 | @Override 12 | public boolean matches(CharSequence charSequence, String s) { 13 | return true; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/common/ServiceError.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.common; 2 | 3 | public enum ServiceError { 4 | 5 | NORMAL(1, "操作成功"), 6 | UN_KNOW_ERROR(-1, "未知错误"), 7 | 8 | /** Global Error */ 9 | GLOBAL_ERR_NO_SIGN_IN(-10001,"未登录或登录过期/Not sign in"), 10 | GLOBAL_ERR_NO_CODE(-10002,"code错误/error code"), 11 | GLOBAL_ERR_NO_AUTHORITY(-10003, "没有操作权限/No operating rights"), 12 | 13 | 14 | ; 15 | 16 | 17 | private int code; 18 | private String msg; 19 | 20 | private ServiceError(int code, String msg) 21 | { 22 | this.code=code; 23 | this.msg=msg; 24 | } 25 | 26 | public int getCode() { 27 | return code; 28 | } 29 | 30 | public String getMsg() { 31 | return msg; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/common/WeChatUrl.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.common; 2 | 3 | public enum WeChatUrl { 4 | 5 | JS_CODE_2_SESSION("https://api.weixin.qq.com/sns/jscode2session") 6 | ,GET_ACCESS_TOKEN("https://api.weixin.qq.com/cgi-bin/token") 7 | ,SEND_TEMPLATE_MESSAGE("https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send") 8 | ; 9 | 10 | private String url; 11 | 12 | WeChatUrl() { 13 | } 14 | 15 | WeChatUrl(String url) { 16 | this.url = url; 17 | } 18 | 19 | public String getUrl() { 20 | return url; 21 | } 22 | 23 | public WeChatUrl setUrl(String url) { 24 | this.url = url; 25 | return this; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/config/DruidConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.config; 2 | 3 | import com.alibaba.druid.pool.DruidDataSource; 4 | import com.alibaba.druid.support.http.StatViewServlet; 5 | import com.alibaba.druid.support.http.WebStatFilter; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 8 | import org.springframework.boot.web.servlet.ServletRegistrationBean; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.PropertySource; 12 | 13 | import javax.sql.DataSource; 14 | 15 | /** 16 | * druid参数配置 17 | * @author jamesluozhiwei 18 | */ 19 | @Configuration 20 | @PropertySource(value = "classpath:application.yml") 21 | public class DruidConfiguration { 22 | 23 | /** 24 | * @author jamesluozhiwei 25 | * @todo 数据源配置 26 | */ 27 | @Bean(destroyMethod = "close", initMethod = "init") 28 | @ConfigurationProperties(prefix = "spring.datasource") 29 | public DataSource druidDataSource() { 30 | DruidDataSource druidDataSource = new DruidDataSource(); 31 | return druidDataSource; 32 | } 33 | 34 | /** 35 | * druid 36 | * 注册一个StatViewServlet 37 | * @return 38 | */ 39 | @Bean 40 | public ServletRegistrationBean druidStatViewServlet(){ 41 | //org.springframework.boot.context.embedded.ServletRegistrationBean提供类的进行注册. 42 | ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(),"/druid/*"); 43 | 44 | //添加初始化参数:initParams 45 | //白名单: 46 | //servletRegistrationBean.addInitParameter("allow","127.0.0.1");//如果不配置或为空则表示允许所有地址访问 47 | //IP黑名单 (存在共同时,deny优先于allow) : 如果满足deny的话提示:Sorry, you are not permitted to view this page. 48 | //servletRegistrationBean.addInitParameter("deny","192.168.1.73"); 49 | //登录查看信息的账号密码. 50 | servletRegistrationBean.addInitParameter("loginUsername","admin"); 51 | servletRegistrationBean.addInitParameter("loginPassword","123456"); 52 | //是否能够重置数据. 53 | servletRegistrationBean.addInitParameter("resetEnable","false"); 54 | return servletRegistrationBean; 55 | } 56 | 57 | /** 58 | * druid过滤器 59 | * 注册一个:filterRegistrationBean 60 | * @return 61 | */ 62 | @Bean 63 | public FilterRegistrationBean druidStatFilter(){ 64 | FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter()); 65 | //添加过滤规则. 66 | filterRegistrationBean.addUrlPatterns("/*"); 67 | //添加不需要忽略的格式信息. 68 | filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); 69 | return filterRegistrationBean; 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/config/SpringSecurityConf.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.config; 2 | 3 | import com.lzw.security.common.NoPasswordEncoder; 4 | import com.lzw.security.filter.JwtAuthenticationTokenFilter; 5 | import com.lzw.security.handler.*; 6 | import com.lzw.security.service.SelfUserDetailsService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.http.HttpMethod; 11 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 12 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 13 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 14 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 15 | import org.springframework.security.config.core.GrantedAuthorityDefaults; 16 | import org.springframework.security.config.http.SessionCreationPolicy; 17 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 18 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 19 | 20 | /** 21 | * @author: jamesluozhiwei 22 | * @description: 23 | */ 24 | @Configuration 25 | @EnableGlobalMethodSecurity(prePostEnabled = true) 26 | public class SpringSecurityConf extends WebSecurityConfigurerAdapter { 27 | 28 | 29 | @Autowired 30 | AjaxAuthenticationEntryPoint authenticationEntryPoint;//未登陆时返回 JSON 格式的数据给前端(否则为 html) 31 | 32 | @Autowired 33 | AjaxAuthenticationSuccessHandler authenticationSuccessHandler; //登录成功返回的 JSON 格式数据给前端(否则为 html) 34 | 35 | @Autowired 36 | AjaxAuthenticationFailureHandler authenticationFailureHandler; //登录失败返回的 JSON 格式数据给前端(否则为 html) 37 | 38 | @Autowired 39 | AjaxLogoutSuccessHandler logoutSuccessHandler;//注销成功返回的 JSON 格式数据给前端(否则为 登录时的 html) 40 | 41 | @Autowired 42 | AjaxAccessDeniedHandler accessDeniedHandler;//无权访问返回的 JSON 格式数据给前端(否则为 403 html 页面) 43 | 44 | @Autowired 45 | SelfUserDetailsService userDetailsService; // 自定义user 46 | 47 | @Autowired 48 | JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // JWT 拦截器 49 | 50 | @Override 51 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 52 | // 加入自定义的安全认证 53 | //auth.authenticationProvider(provider); 54 | auth.userDetailsService(userDetailsService).passwordEncoder(new NoPasswordEncoder());//这里使用自定义的加密方式(不使用加密),security提供了 BCryptPasswordEncoder 加密可自定义或使用这个 55 | } 56 | 57 | @Override 58 | protected void configure(HttpSecurity http) throws Exception { 59 | 60 | // 去掉 CSRF 61 | http.csrf().disable() 62 | .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //关闭session管理,使用token机制处理 63 | .and() 64 | 65 | .httpBasic().authenticationEntryPoint(authenticationEntryPoint) 66 | //.and().antMatcher("/login") 67 | //.and().authorizeRequests().anyRequest().access("@rbacauthorityservice.hasPermission(request,authentication)")// 自定义权限校验 RBAC 动态 url 认证 68 | .and().authorizeRequests().antMatchers(HttpMethod.GET,"/test").hasAuthority("test:list") 69 | .and().authorizeRequests().antMatchers(HttpMethod.POST,"/test").hasAuthority("test:add") 70 | .and().authorizeRequests().antMatchers(HttpMethod.PUT,"/test").hasAuthority("test:update") 71 | .and().authorizeRequests().antMatchers(HttpMethod.DELETE,"/test").hasAuthority("test:delete") 72 | .and().authorizeRequests().antMatchers("/test/*").hasAuthority("test:manager") 73 | .and().authorizeRequests().antMatchers("/login").permitAll() //放行login(这里使用自定义登录) 74 | .and().authorizeRequests().antMatchers("/hello").permitAll(); 75 | 76 | // .and() 77 | // .formLogin() //开启登录, 定义当需要用户登录时候,转到的登录页面 78 | // .loginPage("/test/login.html") 79 | // .loginProcessingUrl("/login") 80 | // .successHandler(authenticationSuccessHandler) // 登录成功 81 | // .failureHandler(authenticationFailureHandler) // 登录失败 82 | // .permitAll() 83 | 84 | // .and() 85 | // .logout()//默认注销行为为logout 86 | // .logoutUrl("/logout") 87 | // .logoutSuccessHandler(logoutSuccessHandler) 88 | // .permitAll(); 89 | 90 | // 记住我 91 | // http.rememberMe().rememberMeParameter("remember-me") 92 | // .userDetailsService(userDetailsService).tokenValiditySeconds(1000); 93 | 94 | http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // 无权访问 JSON 格式的数据 95 | http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // JWT Filter 96 | 97 | } 98 | 99 | @Bean 100 | GrantedAuthorityDefaults grantedAuthorityDefaults(){ 101 | return new GrantedAuthorityDefaults("");//remove the ROLE_ prefix 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/controller/TestController.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.controller; 2 | 3 | import com.lzw.security.common.GenericResponse; 4 | import com.lzw.security.common.ServiceError; 5 | import com.lzw.security.service.WeChatService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.access.prepost.PreAuthorize; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RestController 13 | public class TestController { 14 | 15 | @Autowired 16 | private WeChatService weChatService; 17 | 18 | /** 19 | * code登录获取用户openid 20 | * @param code 21 | * @return 22 | * @throws Exception 23 | */ 24 | @PostMapping("/login") 25 | public GenericResponse login(String code)throws Exception{ 26 | return weChatService.wxLogin(code); 27 | } 28 | 29 | /** 30 | * 权限测试 31 | */ 32 | 33 | @GetMapping("/test") 34 | public GenericResponse test(){ 35 | return GenericResponse.response(ServiceError.NORMAL,"test"); 36 | } 37 | 38 | @PostMapping("/test") 39 | public GenericResponse testPost(){ 40 | return GenericResponse.response(ServiceError.NORMAL,"testPOST"); 41 | } 42 | 43 | @GetMapping("/test/a") 44 | public GenericResponse testA(){ 45 | return GenericResponse.response(ServiceError.NORMAL,"testManage"); 46 | } 47 | 48 | @GetMapping("/hello") 49 | public GenericResponse hello(){ 50 | return GenericResponse.response(ServiceError.NORMAL,"hello security"); 51 | } 52 | 53 | @GetMapping("/ddd") 54 | @PreAuthorize("hasAuthority('ddd:list')") 55 | public GenericResponse ddd(){ 56 | return GenericResponse.response(ServiceError.NORMAL,"dddList"); 57 | } 58 | 59 | @PostMapping("/ddd") 60 | @PreAuthorize("hasAuthority('ddd:add')") 61 | public GenericResponse dddd(){ 62 | return GenericResponse.response(ServiceError.NORMAL,"testPOST"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.entity; 2 | 3 | import org.springframework.security.core.GrantedAuthority; 4 | import org.springframework.security.core.userdetails.UserDetails; 5 | 6 | import java.io.Serializable; 7 | import java.util.Collection; 8 | import java.util.Set; 9 | 10 | public class User implements UserDetails, Serializable { 11 | 12 | private Long id; 13 | 14 | private String username; 15 | 16 | private String password; 17 | 18 | private Set authorities;//权限列表 19 | 20 | @Override 21 | public Collection getAuthorities() { 22 | return this.authorities; 23 | } 24 | 25 | @Override 26 | public String getPassword() { 27 | return this.password; 28 | } 29 | 30 | @Override 31 | public String getUsername() { 32 | return this.username; 33 | } 34 | 35 | @Override 36 | public boolean isAccountNonExpired() { 37 | return true; 38 | } 39 | 40 | @Override 41 | public boolean isAccountNonLocked() { 42 | return true; 43 | } 44 | 45 | @Override 46 | public boolean isCredentialsNonExpired() { 47 | return true; 48 | } 49 | 50 | @Override 51 | public boolean isEnabled() { 52 | return true; 53 | } 54 | 55 | public User setUsername(String username) { 56 | this.username = username; 57 | return this; 58 | } 59 | 60 | public User setPassword(String password) { 61 | this.password = password; 62 | return this; 63 | } 64 | 65 | public User setAuthorities(Set authorities) { 66 | this.authorities = authorities; 67 | return this; 68 | } 69 | 70 | public Long getId() { 71 | return id; 72 | } 73 | 74 | public User setId(Long id) { 75 | this.id = id; 76 | return this; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/filter/JwtAuthenticationTokenFilter.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.filter; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.lzw.security.common.GenericResponse; 5 | import com.lzw.security.common.ServiceError; 6 | import com.lzw.security.entity.User; 7 | import com.lzw.security.service.SelfUserDetailsService; 8 | import com.lzw.security.util.*; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 13 | import org.springframework.security.core.GrantedAuthority; 14 | import org.springframework.security.core.context.SecurityContextHolder; 15 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 16 | import org.springframework.stereotype.Component; 17 | import org.springframework.web.filter.OncePerRequestFilter; 18 | 19 | import javax.servlet.FilterChain; 20 | import javax.servlet.ServletException; 21 | import javax.servlet.http.HttpServletRequest; 22 | import javax.servlet.http.HttpServletResponse; 23 | import java.io.IOException; 24 | import java.util.HashMap; 25 | import java.util.Set; 26 | 27 | /** 28 | * @author: jamesluozhiwei 29 | * @description: 确保在一次请求只通过一次filter,而不需要重复执行 30 | */ 31 | @Component 32 | @Slf4j 33 | public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { 34 | 35 | @Value("${token.expirationMilliSeconds}") 36 | private long expirationMilliSeconds; 37 | 38 | @Autowired 39 | SelfUserDetailsService userDetailsService; 40 | 41 | @Autowired 42 | RedisUtil redisUtil; 43 | 44 | @Override 45 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 46 | String authHeader = request.getHeader("Authorization"); 47 | response.setCharacterEncoding("utf-8"); 48 | if (null == authHeader || !authHeader.startsWith("Bearer ")){ 49 | filterChain.doFilter(request,response);//token格式不正确 50 | return; 51 | } 52 | String authToken = authHeader.substring("Bearer ".length()); 53 | 54 | String subject = JwtTokenUtil.parseToken(authToken);//获取在token中自定义的subject,用作用户标识,用来获取用户权限 55 | 56 | //获取redis中的token信息 57 | 58 | if (!redisUtil.hasKey(authToken)){ 59 | //token 不存在 返回错误信息 60 | response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_SIGN_IN))); 61 | return; 62 | } 63 | 64 | //获取缓存中的信息(根据自己的业务进行拓展) 65 | HashMap hashMap = (HashMap) redisUtil.hget(authToken); 66 | //从tokenInfo中取出用户信息 67 | User user = new User(); 68 | user.setId(Long.parseLong(hashMap.get("id").toString())).setAuthorities((Set) hashMap.get("authorities")); 69 | if (null == hashMap){ 70 | //用户信息不存在或转换错误,返回错误信息 71 | response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_SIGN_IN))); 72 | return; 73 | } 74 | //更新token过期时间 75 | redisUtil.setKeyExpire(authToken,expirationMilliSeconds); 76 | //将信息交给security 77 | UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities()); 78 | authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 79 | SecurityContextHolder.getContext().setAuthentication(authenticationToken); 80 | filterChain.doFilter(request,response); 81 | } 82 | } -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/handler/AjaxAccessDeniedHandler.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.handler; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.lzw.security.common.GenericResponse; 5 | import com.lzw.security.common.ServiceError; 6 | import org.springframework.security.access.AccessDeniedException; 7 | import org.springframework.security.web.access.AccessDeniedHandler; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.servlet.ServletException; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | import java.io.IOException; 14 | 15 | /** 16 | * @author: jamesluozhiwei 17 | * @description: 无权访问 18 | */ 19 | @Component 20 | public class AjaxAccessDeniedHandler implements AccessDeniedHandler { 21 | @Override 22 | public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { 23 | response.setCharacterEncoding("utf-8"); 24 | response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_AUTHORITY))); 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/handler/AjaxAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.handler; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.lzw.security.common.GenericResponse; 5 | import com.lzw.security.common.ServiceError; 6 | import org.springframework.security.core.AuthenticationException; 7 | import org.springframework.security.web.AuthenticationEntryPoint; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.servlet.ServletException; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | import java.io.IOException; 14 | 15 | /** 16 | * @author: jamesluozhiwei 17 | * @description: 用户未登录时返回给前端的数据 18 | */ 19 | @Component 20 | public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint { 21 | 22 | @Override 23 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { 24 | request.setCharacterEncoding("utf-8"); 25 | response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_SIGN_IN))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/handler/AjaxAuthenticationFailureHandler.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.handler; 2 | 3 | 4 | import com.alibaba.fastjson.JSON; 5 | import com.lzw.security.common.GenericResponse; 6 | import com.lzw.security.common.ServiceError; 7 | import org.springframework.security.core.AuthenticationException; 8 | import org.springframework.security.web.authentication.AuthenticationFailureHandler; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.servlet.ServletException; 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.io.IOException; 15 | 16 | /** 17 | * @author: jamesluozhiwei 18 | * @description: 用户登录失败时返回给前端的数据 19 | */ 20 | @Component 21 | public class AjaxAuthenticationFailureHandler implements AuthenticationFailureHandler { 22 | 23 | @Override 24 | public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { 25 | response.setCharacterEncoding("utf-8"); 26 | response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.GLOBAL_ERR_NO_CODE))); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/handler/AjaxAuthenticationSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.handler; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.lzw.security.common.GenericResponse; 5 | import com.lzw.security.common.ServiceError; 6 | import com.lzw.security.util.RedisUtil; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 11 | import org.springframework.stereotype.Component; 12 | 13 | import javax.servlet.ServletException; 14 | import javax.servlet.http.HttpServletRequest; 15 | import javax.servlet.http.HttpServletResponse; 16 | import java.io.IOException; 17 | 18 | /** 19 | * @author: jamesluozhiwei 20 | * @description: 用户登录成功时返回给前端的数据 21 | */ 22 | @Component 23 | @Slf4j 24 | public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler { 25 | 26 | @Autowired 27 | private RedisUtil redisUtil; 28 | 29 | @Override 30 | public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { 31 | //自定义login,不走这里、若使用security的formLogin则自己添加业务实现(生成token、存储token等等) 32 | response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.NORMAL))); 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/handler/AjaxLogoutSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.handler; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.lzw.security.common.GenericResponse; 5 | import com.lzw.security.common.ServiceError; 6 | import com.lzw.security.util.RedisUtil; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; 11 | import org.springframework.stereotype.Component; 12 | 13 | import javax.servlet.ServletException; 14 | import javax.servlet.http.HttpServletRequest; 15 | import javax.servlet.http.HttpServletResponse; 16 | import java.io.IOException; 17 | 18 | /** 19 | * @author: jamesluozhiwei 20 | * @description: 登出成功 21 | */ 22 | @Component 23 | @Slf4j 24 | public class AjaxLogoutSuccessHandler implements LogoutSuccessHandler { 25 | 26 | @Autowired 27 | private RedisUtil redisUtil; 28 | 29 | @Override 30 | public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { 31 | //没有logout不走这里、若使用security的formLogin则自己添加业务实现(移除token等等) 32 | response.getWriter().write(JSON.toJSONString(GenericResponse.response(ServiceError.NORMAL))); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/service/RbacAuthorityService.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.service; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.GrantedAuthority; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.util.AntPathMatcher; 8 | 9 | import javax.servlet.http.HttpServletRequest; 10 | import java.util.Collection; 11 | import java.util.HashSet; 12 | import java.util.Iterator; 13 | import java.util.Set; 14 | 15 | /** 16 | * 鉴权处理 17 | */ 18 | @Component("rbacauthorityservice") 19 | public class RbacAuthorityService { 20 | /** 21 | * 自定义鉴权 22 | * @param request 23 | * @param authentication 用户权限信息 24 | * @return 通过返回true 不通过则返回false(所有鉴权只要有一个通过了则为通过) 25 | */ 26 | public boolean hasPermission(HttpServletRequest request, Authentication authentication) { 27 | Object userInfo = authentication.getPrincipal(); 28 | 29 | boolean hasPermission = false; 30 | 31 | if (userInfo instanceof UserDetails) { 32 | 33 | String username = ((UserDetails) userInfo).getUsername(); 34 | 35 | //获取资源 36 | Set urls = new HashSet(); 37 | // 这些 url 都是要登录后才能访问,且其他的 url 都不能访问! 38 | // 模拟鉴权(可根据自己的业务扩展) 39 | urls.add("/demo/**");//application.yml里设置了项目路径,百度一下我就不贴了 40 | Set set2 = new HashSet(); 41 | Set set3 = new HashSet(); 42 | 43 | AntPathMatcher antPathMatcher = new AntPathMatcher(); 44 | 45 | for (String url : urls) { 46 | if (antPathMatcher.match(url, request.getRequestURI())) { 47 | hasPermission = true; 48 | break; 49 | } 50 | } 51 | return hasPermission; 52 | } else { 53 | return false; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/service/SelfUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.service; 2 | import com.lzw.security.entity.User; 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | 13 | /** 14 | * 用户认证、权限、使用security的表单登录时会被调用(自定义登录请忽略) 15 | * @author: jamesluozhiwei 16 | */ 17 | @Component 18 | @Slf4j 19 | public class SelfUserDetailsService implements UserDetailsService { 20 | 21 | //@Autowired 22 | //private UserMapper userMapper; 23 | 24 | /** 25 | * 若使用security表单鉴权则需实现该方法,通过username获取用户信息(密码、权限等等) 26 | * @param username 27 | * @return 28 | * @throws UsernameNotFoundException 29 | */ 30 | @Override 31 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 32 | 33 | //通过username查询用户 34 | //根据自己的业务获取用户信息 35 | //SelfUserDetails user = userMapper.getUser(username); 36 | //模拟从数据库获取到用户信息 37 | User user = new User(); 38 | if(user == null){ 39 | //仍需要细化处理 40 | throw new UsernameNotFoundException("该用户不存在"); 41 | } 42 | 43 | Set authoritiesSet = new HashSet(); 44 | // 模拟从数据库中获取用户权限 45 | authoritiesSet.add(new SimpleGrantedAuthority("test:list")); 46 | authoritiesSet.add(new SimpleGrantedAuthority("test:add")); 47 | user.setAuthorities(authoritiesSet); 48 | 49 | log.info("用户{}验证通过",username); 50 | return user; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/service/WeChatService.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.service; 2 | 3 | import com.lzw.security.common.GenericResponse; 4 | 5 | /** 6 | * 微信业务接口 7 | */ 8 | public interface WeChatService { 9 | 10 | /** 11 | * 小程序登录 12 | * @param code 13 | * @return 14 | */ 15 | GenericResponse wxLogin(String code)throws Exception; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/service/impl/WeChatServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.service.impl; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.lzw.security.common.GenericResponse; 5 | import com.lzw.security.common.ServiceError; 6 | import com.lzw.security.entity.User; 7 | import com.lzw.security.service.WeChatService; 8 | import com.lzw.security.util.Jcode2SessionUtil; 9 | import com.lzw.security.util.JwtTokenUtil; 10 | import com.lzw.security.util.RedisUtil; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.util.Assert; 17 | 18 | import java.util.HashMap; 19 | import java.util.HashSet; 20 | import java.util.Set; 21 | 22 | /** 23 | * 微信业务实现类 24 | */ 25 | @Service 26 | @Slf4j 27 | public class WeChatServiceImpl implements WeChatService { 28 | 29 | @Value("${weChat.appid}") 30 | private String appid; 31 | 32 | @Value("${weChat.secret}") 33 | private String secret; 34 | 35 | @Autowired 36 | private RedisUtil redisUtil; 37 | 38 | @Override 39 | public GenericResponse wxLogin(String code) throws Exception{ 40 | JSONObject sessionInfo = JSONObject.parseObject(jcode2Session(code)); 41 | 42 | Assert.notNull(sessionInfo,"code 无效"); 43 | 44 | Assert.isTrue(0 == sessionInfo.getInteger("errcode"),sessionInfo.getString("errmsg")); 45 | 46 | // 获取用户唯一标识符 openid成功 47 | // 模拟从数据库获取用户信息 48 | User user = new User(); 49 | user.setId(1L); 50 | Set authoritiesSet = new HashSet(); 51 | // 模拟从数据库中获取用户权限 52 | authoritiesSet.add(new SimpleGrantedAuthority("test:add")); 53 | authoritiesSet.add(new SimpleGrantedAuthority("test:list")); 54 | authoritiesSet.add(new SimpleGrantedAuthority("ddd:list")); 55 | user.setAuthorities(authoritiesSet); 56 | HashMap hashMap = new HashMap<>(); 57 | hashMap.put("id",user.getId().toString()); 58 | hashMap.put("authorities",authoritiesSet); 59 | String token = JwtTokenUtil.generateToken(user); 60 | redisUtil.hset(token,hashMap); 61 | 62 | return GenericResponse.response(ServiceError.NORMAL,token); 63 | } 64 | 65 | /** 66 | * 登录凭证校验 67 | * @param code 68 | * @return 69 | * @throws Exception 70 | */ 71 | private String jcode2Session(String code)throws Exception{ 72 | String sessionInfo = Jcode2SessionUtil.jscode2session(appid,secret,code,"authorization_code");//登录grantType固定 73 | log.info(sessionInfo); 74 | return sessionInfo; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/util/AccessAddressUtil.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.util; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | 7 | /** 8 | * @author: jamesluozhiwei 9 | * @description: 自定义访问地址工具类 10 | * 获取请求的ip地址等信息 11 | */ 12 | @Component 13 | public class AccessAddressUtil { 14 | 15 | /** 16 | * 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址, 17 | * 参考文章: http://developer.51cto.com/art/201111/305181.htm 18 | * 19 | * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢? 20 | * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。 21 | * 22 | * 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130, 23 | * 192.168.1.100 24 | * 25 | * 用户真实IP为: 192.168.1.110 26 | * @param request 27 | * @return 28 | */ 29 | public static String getIpAddress(HttpServletRequest request) { 30 | String ip = request.getHeader("x-forwarded-for"); 31 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 32 | ip = request.getHeader("Proxy-Client-IP"); 33 | } 34 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 35 | ip = request.getHeader("WL-Proxy-Client-IP"); 36 | } 37 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 38 | ip = request.getHeader("HTTP_CLIENT_IP"); 39 | } 40 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 41 | ip = request.getHeader("HTTP_X_FORWARDED_FOR"); 42 | } 43 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 44 | ip = request.getRemoteAddr(); 45 | } 46 | return ip; 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/util/CollectionUtil.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.util; 2 | 3 | 4 | import java.util.Iterator; 5 | import java.util.Map; 6 | 7 | /** 8 | * @author: jamesluoizhiwei 9 | * @description: 集合工具类 10 | * 11 | */ 12 | public class CollectionUtil { 13 | 14 | /** 15 | * @param map 取值的集合 16 | * @param key 所想取值的集合的key 17 | * @return 返回key对应的value 18 | */ 19 | public static String getMapValue(Map map,String key){ 20 | String result = null; 21 | if(map != null){ 22 | Iterator iterable = map.keySet().iterator(); 23 | while (iterable.hasNext()){ 24 | Object object = iterable.next(); 25 | if(key.equals(object)) 26 | if(map.get(object) != null) 27 | result = map.get(object).toString(); 28 | } 29 | } 30 | 31 | return result; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/util/DateUtil.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.util; 2 | 3 | 4 | 5 | import org.springframework.util.StringUtils; 6 | 7 | import java.text.DateFormat; 8 | import java.text.ParseException; 9 | import java.text.SimpleDateFormat; 10 | import java.util.ArrayList; 11 | import java.util.Calendar; 12 | import java.util.Date; 13 | import java.util.List; 14 | 15 | /** 16 | * @author jamesluozhiwei 17 | */ 18 | public class DateUtil { 19 | 20 | /** 21 | * 日期格式 yyyy-MM-dd 22 | */ 23 | public final static String DATEFORMAT = "yyyy-MM-dd"; 24 | /** 25 | * 日期格式 yyyyMMdd 26 | */ 27 | public final static String DATEFORMAT2 = "yyyyMMdd"; 28 | /** 29 | * 日期格式 yyyy/MM/dd 30 | */ 31 | public final static String DATEFORMAT3 = "yyyy/MM/dd"; 32 | /** 33 | * 时间格式 HH:mm:ss 34 | */ 35 | public final static String TIMEFORMAT = "HH:mm:ss"; 36 | /** 37 | * 日期时间格式 yyyy-MM-dd HH:mm:ss 38 | */ 39 | public final static String DATETIMEFORMAT = "yyyy-MM-dd HH:mm:ss"; 40 | 41 | /** 42 | * yyyy-MM 43 | */ 44 | public final static String DATE_SHORT_FORMAT = "yyyy-MM"; 45 | /** 46 | * yyyyMM 47 | */ 48 | public final static String DATE_SHORT_FORMAT2 = "yyyyMM"; 49 | 50 | public final static SimpleDateFormat sdfTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 51 | 52 | public static String DATETIME_FORMAT_REGEX = "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}";//yyyy-MM-dd HH:mm:ss 53 | public static String DATE_FORMAT_REGEX = "\\d{4}-\\d{2}-\\d{2}";//yyyy-MM-dd 54 | public static String SHORTDATE_FORMAT_REGEX = "\\d{4}-\\d{2}";//yyyy-MM 55 | public static String DATE_FORMAT2_REGEX = "\\d{4}\\d{2}\\d{2}";//yyyyMMdd 56 | public static String SHORTDATE_FORMAT2_REGEX = "\\d{4}\\d{2}";//yyyyMM 57 | 58 | /** 59 | * 获取时间日期格式化dateformat 60 | * 61 | * @param pattern 格式 62 | * @return 63 | */ 64 | public static DateFormat getFormat(String pattern) { 65 | if (StringUtils.isEmpty(pattern)) { 66 | return null; 67 | } 68 | return new SimpleDateFormat(pattern); 69 | } 70 | 71 | /** 72 | * 将string类型date格式化为date类型 73 | * 74 | * @param dateString string类型date 75 | * @param pattern 格式 76 | * @return 77 | */ 78 | public static Date getDateFormat(String dateString, String pattern) { 79 | if (StringUtils.isEmpty(dateString)) { 80 | return null; 81 | } 82 | try { 83 | return getFormat(pattern).parse(dateString); 84 | } catch (ParseException e) { 85 | e.printStackTrace(); 86 | return null; 87 | } 88 | } 89 | 90 | public static String getStringFormat(Date date, String pattern) { 91 | if (null == date) { 92 | return null; 93 | } 94 | return getFormat(pattern).format(date); 95 | } 96 | 97 | /** 98 | * 将string类型date重新格式化为string类型 99 | * 100 | * @param dateString 101 | * @param srcPattern 102 | * @param targetPattern 103 | * @return 104 | */ 105 | public static String getStringReformat(String dateString, String srcPattern, String targetPattern) { 106 | Date date = getDateFormat(dateString, srcPattern); 107 | return getStringFormat(date, targetPattern); 108 | } 109 | 110 | /** 111 | * 获取当前时间的前一天 112 | * 113 | * @return 114 | */ 115 | public static Date getYesterday() { 116 | Calendar calendar = Calendar.getInstance(); 117 | calendar.add(Calendar.DATE, -1); 118 | calendar.set(Calendar.HOUR_OF_DAY, 0); 119 | calendar.set(Calendar.MINUTE, 0); 120 | calendar.set(Calendar.SECOND, 0); 121 | return calendar.getTime(); 122 | } 123 | 124 | /** 125 | * 获取本月的第一天 126 | * 127 | * @return 128 | */ 129 | public static Date getFirstDayByMonth() { 130 | Calendar calendar = Calendar.getInstance(); 131 | calendar.set(Calendar.DAY_OF_MONTH, 1); 132 | calendar.set(Calendar.HOUR_OF_DAY, 0); 133 | calendar.set(Calendar.MINUTE, 0); 134 | calendar.set(Calendar.SECOND, 0); 135 | return calendar.getTime(); 136 | } 137 | 138 | /** 139 | * 获取上一个月的第一天 140 | * 141 | * @return 142 | */ 143 | public static Date getFirstDayByLastMonth() { 144 | Calendar calendar = Calendar.getInstance(); 145 | calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) - 1); 146 | calendar.set(Calendar.DAY_OF_MONTH, 1); 147 | calendar.set(Calendar.HOUR_OF_DAY, 0); 148 | calendar.set(Calendar.MINUTE, 0); 149 | calendar.set(Calendar.SECOND, 0); 150 | return calendar.getTime(); 151 | } 152 | 153 | /** 154 | * 根据月份获取当月第一天 155 | * 156 | * @param month 157 | * @return 158 | */ 159 | public static Date getFirstDayByMonth(int month) { 160 | Calendar calendar = Calendar.getInstance(); 161 | calendar.set(Calendar.MONTH, month); 162 | calendar.set(Calendar.DAY_OF_MONTH, 1); 163 | calendar.set(Calendar.HOUR_OF_DAY, 0); 164 | calendar.set(Calendar.MINUTE, 0); 165 | calendar.set(Calendar.SECOND, 0); 166 | return calendar.getTime(); 167 | } 168 | 169 | /** 170 | * 获取某个月份的第一天 171 | * 172 | * @param month 173 | * @return 174 | */ 175 | public static Date getFirstDayByMonth(Date date, int month) { 176 | Calendar calendar = Calendar.getInstance(); 177 | calendar.setTime(date); 178 | calendar.set(Calendar.MONTH, month); 179 | calendar.set(Calendar.DAY_OF_MONTH, 1); 180 | calendar.set(Calendar.HOUR_OF_DAY, 0); 181 | calendar.set(Calendar.MINUTE, 0); 182 | calendar.set(Calendar.SECOND, 0); 183 | return calendar.getTime(); 184 | } 185 | 186 | /** 187 | * 计算两个日期之间相隔的月份 188 | * 189 | * @param startDate 190 | * @param endDate 191 | * @return 192 | */ 193 | public static List intervalMonths(Date startDate, Date endDate) { 194 | List monthList = new ArrayList(); 195 | 196 | Calendar tmp = Calendar.getInstance(); 197 | tmp.setTime(startDate); 198 | tmp.set(Calendar.DAY_OF_MONTH, tmp.getActualMaximum(Calendar.DAY_OF_MONTH) - 1); 199 | 200 | Calendar start = Calendar.getInstance(); 201 | start.setTime(startDate); 202 | start.set(Calendar.DAY_OF_MONTH, 1); 203 | 204 | Calendar end = Calendar.getInstance(); 205 | end.setTime(endDate); 206 | end.set(Calendar.DAY_OF_MONTH, end.getActualMaximum(Calendar.DAY_OF_MONTH)); 207 | 208 | SimpleDateFormat sdf = new SimpleDateFormat(DATE_SHORT_FORMAT); 209 | for (; tmp.after(start) && tmp.before(end); tmp.add(Calendar.MONTH, 1), tmp.set(Calendar.DAY_OF_MONTH, tmp.getActualMaximum(Calendar.DAY_OF_MONTH) - 1)) { 210 | monthList.add(sdf.format(tmp.getTime())); 211 | } 212 | return monthList; 213 | } 214 | 215 | /** 216 | * 传入时间和当前时间比较最大获取上个月,返回虽然Date,实际时间是yyyyMM01 00:00:00.0 217 | * 218 | * @param date 219 | * @return 220 | */ 221 | public static Date maxLastMonth(Date date) { 222 | Date thisMonth = DateUtil.getDateFormat(DateUtil.getStringFormat(new Date(), DateUtil.DATE_SHORT_FORMAT), DateUtil.DATE_SHORT_FORMAT); 223 | if (thisMonth.compareTo(date) <= 0) { 224 | Calendar calendar = Calendar.getInstance(); 225 | calendar.setTime(thisMonth); 226 | calendar.add(Calendar.MONTH, -1); 227 | return calendar.getTime(); 228 | } 229 | return date; 230 | } 231 | 232 | public static Date getMonth(Date date, int month) { 233 | Calendar calendar = Calendar.getInstance(); 234 | calendar.setTime(date); 235 | calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) + month); 236 | return calendar.getTime(); 237 | } 238 | 239 | public static Date getFirstDayByMonth(Date date) { 240 | Calendar calendar = Calendar.getInstance(); 241 | calendar.setTime(date); 242 | calendar.set(Calendar.DAY_OF_MONTH, 1); 243 | calendar.set(Calendar.HOUR_OF_DAY, 0); 244 | calendar.set(Calendar.MINUTE, 0); 245 | calendar.set(Calendar.SECOND, 0); 246 | return calendar.getTime(); 247 | } 248 | 249 | public static int betweenDays(Date beforeDate, Date afterDate) { 250 | if (null == beforeDate || null == afterDate) { 251 | throw new NullPointerException("date can't be null"); 252 | } 253 | if (beforeDate.after(afterDate)) { 254 | return 0; 255 | } 256 | return (int) ((afterDate.getTime() - beforeDate.getTime()) / (1000 * 3600 * 24)); 257 | } 258 | 259 | /** 260 | * 获取当前时间的后i天 261 | * 精确到秒 262 | * @param i 263 | * @return 264 | */ 265 | public static String getAddDayTime(int i){ 266 | Date date = new Date(System.currentTimeMillis()+i*24*60*60*1000); 267 | return sdfTime.format(date); 268 | } 269 | 270 | /** 271 | * 获取当前时间的+多少秒 272 | * 精确到秒 273 | * @param i 274 | * @return 275 | */ 276 | public static String getAddDaySecond(int i){ 277 | Date date = new Date(System.currentTimeMillis()+i*1000); 278 | return sdfTime.format(date); 279 | } 280 | 281 | /** 282 | * 获取当前时间的YYYY-MM-DD HH:mm:ss格式 283 | * 284 | * @return 285 | */ 286 | public static String getTime() { 287 | return sdfTime.format(new Date()); 288 | } 289 | 290 | /** 291 | * 日期比较,如果s>=e 返回true 否则返回false 292 | * @param s 293 | * @param e 294 | * @return 295 | */ 296 | public static boolean compareDate(String s, String e) { 297 | if(formatDate(s)==null||formatDate(e)==null){ 298 | return false; 299 | } 300 | // return fomatDate(s).getTime() >=fomatDate(e).getTime(); 301 | return s.compareTo(e)>0; 302 | } 303 | 304 | /** 305 | * 格式化日期 306 | * @param date 307 | * @return 308 | */ 309 | public static Date formatDate(String date) { 310 | DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd"); 311 | try { 312 | return fmt.parse(date); 313 | } catch (ParseException e) { 314 | e.printStackTrace(); 315 | return null; 316 | } 317 | } 318 | } 319 | 320 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/util/HttpUtil.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.util; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import org.apache.http.HttpEntity; 5 | import org.apache.http.HttpResponse; 6 | import org.apache.http.client.methods.HttpGet; 7 | import org.apache.http.client.methods.HttpPost; 8 | import org.apache.http.entity.StringEntity; 9 | import org.apache.http.impl.client.DefaultHttpClient; 10 | import org.apache.http.util.EntityUtils; 11 | 12 | import java.io.*; 13 | import java.net.HttpURLConnection; 14 | import java.net.URL; 15 | import java.util.Base64; 16 | 17 | /** 18 | * 请求工具类 19 | * @author jamesluozhiwei 20 | */ 21 | public class HttpUtil { 22 | 23 | /** 24 | * 发送get请求 25 | * @param url 26 | * @return 27 | */ 28 | public static String sendGet(String url){ 29 | DefaultHttpClient httpClient = new DefaultHttpClient(); 30 | HttpGet httpGet = new HttpGet(url); 31 | String result = null; 32 | try { 33 | HttpResponse response = httpClient.execute(httpGet); 34 | HttpEntity entity = response.getEntity(); 35 | if (entity != null) { 36 | result = EntityUtils.toString(entity, "UTF-8"); 37 | } 38 | httpGet.releaseConnection(); 39 | } catch (IOException e) { 40 | e.printStackTrace(); 41 | } 42 | return result; 43 | } 44 | 45 | 46 | /** 47 | * 发送post请求 48 | * @param url 49 | * @param params 可使用JSONObject转JSON字符串 50 | * @return 51 | */ 52 | public static String sendPost(String url,String params){ 53 | DefaultHttpClient httpClient = new DefaultHttpClient(); 54 | HttpPost httpPost = new HttpPost(url); 55 | JSONObject jsonObject = null; 56 | try { 57 | httpPost.setEntity(new StringEntity(params, "UTF-8")); 58 | HttpResponse response = httpClient.execute(httpPost); 59 | return EntityUtils.toString(response.getEntity(),"UTF-8"); 60 | } catch (IOException e) { 61 | e.printStackTrace(); 62 | } 63 | return null; 64 | } 65 | 66 | /** 67 | * 发送post请求 68 | * @param httpUrl 69 | * @param param JSON字符串 70 | * @return 71 | */ 72 | public static String doPostBase64(String httpUrl, String param) { 73 | 74 | HttpURLConnection connection = null; 75 | InputStream is = null; 76 | OutputStream os = null; 77 | BufferedReader br = null; 78 | String result = null; 79 | try { 80 | URL url = new URL(httpUrl); 81 | // 通过远程url连接对象打开连接 82 | connection = (HttpURLConnection) url.openConnection(); 83 | // 设置连接请求方式 84 | connection.setRequestMethod("POST"); 85 | // 设置连接主机服务器超时时间:15000毫秒 86 | connection.setConnectTimeout(15000); 87 | // 设置读取主机服务器返回数据超时时间:60000毫秒 88 | connection.setReadTimeout(60000); 89 | 90 | // 默认值为:false,当向远程服务器传送数据/写数据时,需要设置为true 91 | connection.setDoOutput(true); 92 | // 默认值为:true,当前向远程服务读取数据时,设置为true,该参数可有可无 93 | connection.setDoInput(true); 94 | // 设置传入参数的格式:请求参数应该是 name1=value1&name2=value2 的形式。 95 | connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); 96 | // 通过连接对象获取一个输出流 97 | os = connection.getOutputStream(); 98 | // 通过输出流对象将参数写出去/传输出去,它是通过字节数组写出的 99 | 100 | os.write(param.getBytes()); 101 | 102 | // 通过连接对象获取一个输入流,向远程读取 103 | if (connection.getResponseCode() == 200) { 104 | 105 | is = connection.getInputStream(); 106 | 107 | ByteArrayOutputStream swapStream = new ByteArrayOutputStream(); 108 | byte[] buff = new byte[100]; 109 | int rc = 0; 110 | while ((rc = is.read(buff, 0, 100)) > 0) { 111 | swapStream.write(buff, 0, rc); 112 | } 113 | byte[] in2b = swapStream.toByteArray(); 114 | String tmp = new String(in2b); 115 | if (tmp.indexOf("errcode") == -1) 116 | return Base64.getEncoder().encodeToString(in2b); 117 | return tmp; 118 | } 119 | } catch (Exception e) { 120 | e.printStackTrace(); 121 | } finally { 122 | // 关闭资源 123 | if (null != br) { 124 | try { 125 | br.close(); 126 | } catch (IOException e) { 127 | e.printStackTrace(); 128 | } 129 | } 130 | if (null != os) { 131 | try { 132 | os.close(); 133 | } catch (IOException e) { 134 | e.printStackTrace(); 135 | } 136 | } 137 | if (null != is) { 138 | try { 139 | is.close(); 140 | } catch (IOException e) { 141 | e.printStackTrace(); 142 | } 143 | } 144 | // 断开与远程地址url的连接 145 | connection.disconnect(); 146 | } 147 | return result; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/util/Jcode2SessionUtil.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.util; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.lzw.security.common.WeChatUrl; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.apache.http.NameValuePair; 7 | import org.apache.http.client.entity.UrlEncodedFormEntity; 8 | import org.apache.http.client.methods.HttpPost; 9 | import org.apache.http.impl.client.DefaultHttpClient; 10 | import org.apache.http.message.BasicNameValuePair; 11 | import org.apache.http.util.EntityUtils; 12 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 13 | import org.codehaus.xfire.util.Base64; 14 | 15 | import javax.crypto.Cipher; 16 | import javax.crypto.spec.IvParameterSpec; 17 | import javax.crypto.spec.SecretKeySpec; 18 | import java.security.AlgorithmParameters; 19 | import java.security.Security; 20 | import java.util.ArrayList; 21 | import java.util.Arrays; 22 | import java.util.List; 23 | 24 | @Slf4j 25 | public class Jcode2SessionUtil { 26 | 27 | /** 28 | * 请求微信后台获取用户数据 29 | * @param code wx.login获取到的临时code 30 | * @return 请求结果 31 | * @throws Exception 32 | */ 33 | public static String jscode2session(String appid,String secret,String code,String grantType)throws Exception{ 34 | //定义返回的json对象 35 | JSONObject result = new JSONObject(); 36 | //创建请求通过code换取session等数据 37 | HttpPost httpPost = new HttpPost(WeChatUrl.JS_CODE_2_SESSION.getUrl()); 38 | List params=new ArrayList(); 39 | //建立一个NameValuePair数组,用于存储欲传送的参数 40 | params.add(new BasicNameValuePair("appid",appid)); 41 | params.add(new BasicNameValuePair("secret",secret)); 42 | params.add(new BasicNameValuePair("js_code",code)); 43 | params.add(new BasicNameValuePair("grant_type",grantType)); 44 | //设置编码 45 | httpPost.setEntity(new UrlEncodedFormEntity(params));//添加参数 46 | return EntityUtils.toString(new DefaultHttpClient().execute(httpPost).getEntity()); 47 | } 48 | /** 49 | * 解密用户敏感数据获取用户信息 50 | * @param sessionKey 数据进行加密签名的密钥 51 | * @param encryptedData 包括敏感数据在内的完整用户信息的加密数据 52 | * @param iv 加密算法的初始向量 53 | * @return 54 | */ 55 | public static String getUserInfo(String encryptedData,String sessionKey,String iv)throws Exception{ 56 | // 被加密的数据 57 | byte[] dataByte = Base64.decode(encryptedData); 58 | // 加密秘钥 59 | byte[] keyByte = Base64.decode(sessionKey); 60 | // 偏移量 61 | byte[] ivByte = Base64.decode(iv); 62 | // 如果密钥不足16位,那么就补足. 这个if 中的内容很重要 63 | int base = 16; 64 | if (keyByte.length % base != 0) { 65 | int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0); 66 | byte[] temp = new byte[groups * base]; 67 | Arrays.fill(temp, (byte) 0); 68 | System.arraycopy(keyByte, 0, temp, 0, keyByte.length); 69 | keyByte = temp; 70 | } 71 | // 初始化 72 | Security.addProvider(new BouncyCastleProvider()); 73 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding","BC"); 74 | SecretKeySpec spec = new SecretKeySpec(keyByte, "AES"); 75 | AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES"); 76 | parameters.init(new IvParameterSpec(ivByte)); 77 | cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化 78 | byte[] resultByte = cipher.doFinal(dataByte); 79 | if (null != resultByte && resultByte.length > 0) { 80 | String result = new String(resultByte, "UTF-8"); 81 | log.info(result); 82 | return result; 83 | } 84 | return null; 85 | } 86 | 87 | /** 88 | * 获取微信接口调用凭证 89 | * @param appid 90 | * @param secret 91 | * @return 返回String 可转JSON 92 | */ 93 | public static String getAccessToken(String appid,String secret){ 94 | JSONObject params = new JSONObject(); 95 | params.put("grant_type","client_credential");//获取接口调用凭证 96 | params.put("appid",appid); 97 | params.put("secret",secret); 98 | return HttpUtil.sendGet(WeChatUrl.GET_ACCESS_TOKEN.getUrl()+"?grant_type=client_credential&appid=" + appid + "&secret=" + secret); 99 | } 100 | 101 | /** 102 | * 发送模板消息 103 | * @param access_token 接口调用凭证 104 | * @param touser 接收者(用户)的 openid 105 | * @param template_id 所需下发的模板消息id 106 | * @param page 点击模版卡片后跳转的页面,仅限本小程序内的页面。支持带参数,(eg:index?foo=bar)。该字段不填则模版无法跳转 107 | * @param form_id 表单提交场景下,为submit事件带上的formId;支付场景下,为本次支付的 prepay_id 108 | * @param data 模版内容,不填则下发空模版。具体格式请参照官网示例 109 | * @param emphasis_keyword 模版需要放大的关键词,不填则默认无放大 110 | * @return 返回String可转JSON 111 | */ 112 | public static String sendTemplateMessage(String access_token,String touser,String template_id,String page,String form_id,Object data,String emphasis_keyword){ 113 | JSONObject params = new JSONObject(); 114 | params.put("touser",touser); 115 | params.put("template_id",template_id); 116 | if (null != page && !"".equals(page)){ 117 | params.put("page",page); 118 | } 119 | params.put("form_id",form_id); 120 | params.put("data",data); 121 | if (null != emphasis_keyword && !"".equals(emphasis_keyword)){ 122 | params.put("emphasis_keyword",emphasis_keyword); 123 | } 124 | //发送请求 125 | return HttpUtil.sendPost(WeChatUrl.SEND_TEMPLATE_MESSAGE.getUrl() + "?access_token=" + access_token,params.toString()); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/util/JwtTokenUtil.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.util; 2 | 3 | 4 | import com.lzw.security.entity.User; 5 | import io.jsonwebtoken.Claims; 6 | import io.jsonwebtoken.Jwts; 7 | import io.jsonwebtoken.SignatureAlgorithm; 8 | 9 | import java.util.Date; 10 | import java.util.Map; 11 | 12 | /** 13 | * @author: jamesluozhiwei 14 | * @description: jwt生成token 15 | */ 16 | public class JwtTokenUtil { 17 | 18 | private static final String SALT = "123456";//加密解密盐值 19 | 20 | /** 21 | * 生成token(请根据自身业务扩展) 22 | * @param subject (主体信息) 23 | * @param expirationSeconds 过期时间(秒) 24 | * @param claims 自定义身份信息 25 | * @return 26 | */ 27 | public static String generateToken(String subject, int expirationSeconds, Map claims) { 28 | return Jwts.builder() 29 | .setClaims(claims) 30 | .setSubject(subject)//主题 31 | //.setExpiration(new Date(System.currentTimeMillis() + expirationSeconds * 1000)) 32 | .signWith(SignatureAlgorithm.HS512, SALT) // 不使用公钥私钥 33 | //.signWith(SignatureAlgorithm.RS256, privateKey) 34 | .compact(); 35 | } 36 | 37 | /** 38 | * 生成token 39 | * @param user 40 | * @return 41 | */ 42 | public static String generateToken(User user){ 43 | return Jwts.builder() 44 | .setSubject(user.getId().toString()) 45 | .setExpiration(new Date(System.currentTimeMillis())) 46 | .setIssuedAt(new Date()) 47 | .setIssuer("JAMES") 48 | .signWith(SignatureAlgorithm.HS512, SALT)// 不使用公钥私钥 49 | .compact(); 50 | } 51 | 52 | /** 53 | * 解析token,获得subject中的信息 54 | * @param token 55 | * @return 56 | */ 57 | public static String parseToken(String token) { 58 | String subject = null; 59 | try { 60 | subject = getTokenBody(token).getSubject(); 61 | } catch (Exception e) { 62 | } 63 | return subject; 64 | } 65 | 66 | /** 67 | * 获取token自定义属性 68 | * @param token 69 | * @return 70 | */ 71 | public static Map getClaims(String token){ 72 | Map claims = null; 73 | try { 74 | claims = getTokenBody(token); 75 | }catch (Exception e) { 76 | } 77 | 78 | return claims; 79 | } 80 | 81 | 82 | /** 83 | * 解析token 84 | * @param token 85 | * @return 86 | */ 87 | private static Claims getTokenBody(String token){ 88 | return Jwts.parser() 89 | //.setSigningKey(publicKey) 90 | .setSigningKey(SALT) 91 | .parseClaimsJws(token) 92 | .getBody(); 93 | } 94 | } -------------------------------------------------------------------------------- /src/main/java/com/lzw/security/util/RedisUtil.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security.util; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.data.redis.core.RedisTemplate; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.HashMap; 9 | import java.util.Set; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | /** 13 | * redis工具类 14 | * @author: jamesluozhiwei 15 | */ 16 | @Component 17 | public class RedisUtil { 18 | 19 | @Value("${token.expirationMilliSeconds}") 20 | private long expirationMilliSeconds; 21 | 22 | //@Autowired 23 | //private StringRedisTemplate redisTemplate; 24 | 25 | @Autowired 26 | private RedisTemplate redisTemplate; 27 | 28 | /** 29 | * 查询key,支持模糊查询 30 | * @param key 31 | * */ 32 | public Set keys(String key){ 33 | return redisTemplate.keys(key); 34 | } 35 | 36 | /** 37 | * 字符串获取值 38 | * @param key 39 | * */ 40 | public Object get(String key){ 41 | return redisTemplate.opsForValue().get(key); 42 | } 43 | 44 | /** 45 | * 字符串存入值 46 | * 默认过期时间为2小时 47 | * @param key 48 | * */ 49 | public void set(String key, String value){ 50 | set(key,value,expirationMilliSeconds); 51 | } 52 | 53 | /** 54 | * 字符串存入值 55 | * @param expire 过期时间(毫秒计) 56 | * @param key 57 | * */ 58 | public void set(String key, String value,long expire){ 59 | redisTemplate.opsForValue().set(key,value, expire,TimeUnit.MILLISECONDS); 60 | } 61 | 62 | /** 63 | * 删出key 64 | * 这里跟下边deleteKey()最底层实现都是一样的,应该可以通用 65 | * @param key 66 | * */ 67 | public void delete(String key){ 68 | redisTemplate.opsForValue().getOperations().delete(key); 69 | } 70 | 71 | /** 72 | * 添加单个 73 | * @param key key 74 | * @param filed filed 75 | * @param domain 对象 76 | */ 77 | public void hset(String key,String filed,Object domain){ 78 | hset(key,filed,domain,expirationMilliSeconds); 79 | } 80 | 81 | /** 82 | * 添加单个 83 | * @param key key 84 | * @param filed filed 85 | * @param domain 对象 86 | * @param expire 过期时间(毫秒计) 87 | */ 88 | public void hset(String key,String filed,Object domain,long expire){ 89 | redisTemplate.opsForHash().put(key, filed, domain); 90 | setKeyExpire(key,expirationMilliSeconds); 91 | } 92 | 93 | /** 94 | * 添加HashMap 95 | * 96 | * @param key key 97 | * @param hm 要存入的hash表 98 | */ 99 | public void hset(String key, HashMap hm){ 100 | redisTemplate.opsForHash().putAll(key,hm); 101 | setKeyExpire(key,expirationMilliSeconds); 102 | } 103 | 104 | /** 105 | * 如果key存在就不覆盖 106 | * @param key 107 | * @param filed 108 | * @param domain 109 | */ 110 | public void hsetAbsent(String key,String filed,Object domain){ 111 | redisTemplate.opsForHash().putIfAbsent(key, filed, domain); 112 | } 113 | 114 | /** 115 | * 查询key和field所确定的值 116 | * @param key 查询的key 117 | * @param field 查询的field 118 | * @return HV 119 | */ 120 | public Object hget(String key,String field) { 121 | return redisTemplate.opsForHash().get(key, field); 122 | } 123 | 124 | /** 125 | * 查询该key下所有值 126 | * @param key 查询的key 127 | * @return Map 128 | */ 129 | public Object hget(String key) { 130 | return redisTemplate.opsForHash().entries(key); 131 | } 132 | 133 | /** 134 | * 删除key下所有值 135 | * 136 | * @param key 查询的key 137 | */ 138 | public void deleteKey(String key) { 139 | redisTemplate.opsForHash().getOperations().delete(key); 140 | } 141 | 142 | /** 143 | * 添加set集合 144 | * @param key 145 | * @param set 146 | * @param expire 147 | */ 148 | public void sset(Object key,Set set,long expire){ 149 | redisTemplate.opsForSet().add(key,set); 150 | setKeyExpire(key,expire); 151 | } 152 | 153 | /** 154 | * 添加set集合 155 | * @param key 156 | * @param set 157 | */ 158 | public void sset(Object key,Set set){ 159 | sset(key, set,expirationMilliSeconds); 160 | } 161 | 162 | /** 163 | * 判断key和field下是否有值 164 | * @param key 判断的key 165 | * @param field 判断的field 166 | */ 167 | public Boolean hasKey(String key,String field) { 168 | return redisTemplate.opsForHash().hasKey(key,field); 169 | } 170 | 171 | /** 172 | * 判断key下是否有值 173 | * @param key 判断的key 174 | */ 175 | public Boolean hasKey(String key) { 176 | return redisTemplate.opsForHash().getOperations().hasKey(key); 177 | } 178 | 179 | /** 180 | * 更新key的过期时间 181 | * @param key 182 | * @param expire 183 | */ 184 | public void setKeyExpire(Object key,long expire){ 185 | redisTemplate.expire(key,expire,TimeUnit.MILLISECONDS); 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | username: root 4 | password: 123456 5 | url: jdbc:mysql://localhost:3306/db_XXX?characterEncoding=utf-8&useSSl=false 6 | driver-class-name: com.mysql.jdbc.Driver 7 | type: com.alibaba.druid.pool.DruidDataSource 8 | #监控统计拦截的filters 9 | filters: stat,wall,log4j 10 | #druid配置 11 | #配置初始化大小/最小/最大 12 | initialSize: 5 13 | minIdle: 5 14 | maxActive: 20 15 | #获取连接等待超时时间 16 | maxWait: 60000 17 | #间隔多久进行一次检测,检测需要关闭的空闲连接 18 | timeBetweenEvictionRunsMillis: 60000 19 | #一个连接在池中最小生存的时间 20 | minEvictableIdleTimeMillis: 300000 21 | validationQuery: SELECT 1 FROM DUAL 22 | testWhileIdle: true 23 | testOnBorrow: false 24 | testOnReturn: false 25 | #打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false 26 | poolPreparedStatements: false 27 | maxPoolPreparedStatementPerConnectionSize: 20 28 | # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 29 | connectionProperties: 30 | druid: 31 | stat: 32 | mergeSql: true 33 | slowSqlMillis: 5000 34 | http: 35 | encoding: 36 | charset: utf-8 37 | force: true 38 | enabled: true 39 | redis: 40 | host: 127.0.0.1 41 | port: 6379 42 | password: 123456 43 | 44 | 45 | #mybatis是独立节点,需要单独配置 46 | mybatis: 47 | mapper-locations: classpath*:mapper/*.xml 48 | type-aliases-package: com.lzw.security.entity 49 | configuration: 50 | map-underscore-to-camel-case: true 51 | 52 | server: 53 | port: 8080 54 | tomcat: 55 | uri-encoding: utf-8 56 | servlet: 57 | context-path: / 58 | 59 | #自定义参数,可以迁移走 60 | token: 61 | #redis默认过期时间(2小时)(这是自定义的)(毫秒) 62 | expirationMilliSeconds: 7200000 63 | 64 | #微信相关参数 65 | weChat: 66 | #小程序appid 67 | appid: aaaaaaaaaaaaaaaa 68 | #小程序密钥 69 | secret: ssssssssssssssss 70 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /src/test/java/com/lzw/security/SecurityApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.lzw.security; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class SecurityApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | --------------------------------------------------------------------------------