Spring boot 2.0 整合 Security JWT 前后分离 认证授权

Spring Security 简介

Spring Security 主要作用是认证与授权
Spring Security 和 jwt 相关的介绍自行百度吧

下面直接上代码,注意看注释 有相关的代码作用解释,如果有错误的地方,请指出 一起学习 谢谢!

认证大概流程

认证流程.jpg

结构目录

image.png

pom.xml

<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <security-jwt.version>1.0.9.RELEASE</security-jwt.version>
        <jjwt.version>0.9.0</jjwt.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>${security-jwt.version}</version>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
   
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

application.yml

server:
  port: 18081

spring:
  application:
    name: oauth-server   # 应用名称

  jpa:
      open-in-view: true
      database: POSTGRESQL
      show-sql: true
      hibernate:
        ddl-auto: update
        dialect: org.hibernate.dialect.PostgreSQLDialect
      properties:
        hibernate:
          temp:
            use_jdbc_metadata_defaults: false

  # 数据源 配置
  datasource:
      platform: postgres
      url: jdbc:postgresql://127.0.0.1:5432/cloud_oauth2?useUnicode=true&characterEncoding=utf-8
      username: postgres
      password: postgres123
      driver-class-name: org.postgresql.Driver

  # redis 配置
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    database: 0
    jedis:
      pool:
        #最大连接数
        max-active: 8
        #最大空闲
        max-idle: 8
        #最大阻塞等待时间(负数表示没限制)
        max-wait: -1ms
        #最小空闲
        min-idle: 0
    #连接超时时间
    timeout: 1000ms



# JWT 配置
jwt:
  # 存放Token的Header Key
  header: Authorization
  # 密匙key
  secret: mySecret
  # 过期时间   单位秒 7天后过期  604800
  expiration: 3600
  # 自定义token 前缀字符
  tokenHead: Bearer-
  # 超时时间   单位秒
  access_token: 3600
  # 刷新token时间   单位秒
  refresh_token: 3600
  route:
    authentication:
      path: login/entry
      refresh: oauth/refresh
      register: login/account


# 配置不需要认证的接口
com:
  example:
    oauth:
      security:
        antMatchers:
          /auth/v1/api/login/**,
          /auth/v1/api/module/tree/**,
          /auth/v1/api/grid/**


# 日志
logging:
  level:
    org:
      springframework:
        security: DEBUG

必须的配置类

  • web安全配置类 WebSecurityConfig
  • 用户身份权限认证类 MyUserDetailService
  • 资源权限认证器 MyAccessDecisionManager
  • 请求过滤类 MyFilterSecurityInterceptor
  • 加载资源与权限的关系 MyInvocationSecurityMetadataSourceService

可选的处理类

  • 权限不足处理类 MyAccessDeniedHandler
  • 异常处理类 MyAuthenticationException
  • 登录成功后处理类 MyAuthenticationSuccessHandler
  • 登录失败后处理类 MyAuthenticationFailureHandler
  • 退出系统成功后处理类 MyLogoutSuccessHandler
  • 登录验证(比如校验验证码) MyUsernamePasswordAuthenticationFilter
  • 认证失败处理类 MyAuthenticationEntryPointHandler

整合jwt 需要的类

  • jwt 工具类 提供校验toeken 、生成token、根据token获取用户等方法 JwtTokenUtil
  • 用户信息 JWTUserDetails
  • JWTUserDetailsFactory
  • 对请求的token进行校验 JwtAuthenticationTokenFilter

MyAccessDecisionManager 资源权限认证器 认证用户是否拥有所请求资源的权限

/***
 *
 * @FileName: MyAccessDecisionManager
 * @Company:
 * @author    
 * @Date      2018年05月11日
 * @version   1.0.0
 * @remark:   资源权限认证器  证用户是否拥有所请求资源的权限
 * @explain   接口AccessDecisionManager也是必须实现的。 decide方法里面写的就是授权策略了,需要什么策略,可以自己写其中的策略逻辑
 *             认证通过就返回,不通过抛异常就行了,spring security会自动跳到权限不足处理类(WebSecurityConfig 类中 配置文件上配的)
 *
 *
 */
@Slf4j
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {

    /**
     *  授权策略
     *
     * decide()方法在url请求时才会调用,服务器启动时不会执行这个方法
     *
     * @param configAttributes 装载了请求的url允许的角色数组 。这里是从MyInvocationSecurityMetadataSource里的loadResourceDefine方法里的atts对象取出的角色数据赋予给了configAttributes对象
     * @param object url
     * @param authentication 装载了从数据库读出来的权限(角色) 数据。这里是从MyUserDetailService里的loadUserByUsername方法里的grantedAuths对象的值传过来给 authentication 对象,简单点就是从spring的全局缓存SecurityContextHolder中拿到的,里面是用户的权限信息
     *
     * 注意: Authentication authentication 如果是前后端分离 则有跨域问题,跨域情况下 authentication 无法获取当前登陆人的身份认证(登陆成功后),我尝试用token来效验权限
     *
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        // 无权限访问
        if(CollectionUtils.isEmpty(configAttributes)){
             log.info("无访问权限.");
            throw new AccessDeniedException("无访问权限.");
        }
        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while (iterator.hasNext()){
            ConfigAttribute configAttribute = iterator.next();
            String needRole = configAttribute.getAttribute();
            for(GrantedAuthority grantedAuthority : authentication.getAuthorities()){
                //grantedAuthority 为用户所被赋予的权限。 needRole 为访问相应的资源应该具有的权限。
                //判断两个请求的url的权限和用户具有的权限是否相同,如相同,允许访问 权限就是那些以ROLE_为前缀的角色
                if (needRole.trim().equals(grantedAuthority.getAuthority().trim())){
                    //匹配到对应的角色,则允许通过
                    return;
                }
            }
        }
        //该url具有访问权限,但是当前登录用户没有匹配到URL对应的权限,则抛出无权限错误
        log.info("无访问权限.");
        throw  new AccessDeniedException("无访问权限.");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

MyInvocationSecurityMetadataSourceService 加载资源与权限的对应关系

/***
 *
 * @FileName: MyInvocationSecurityMetadataSourceService
 * @Company:
 * @author    
 * @Date      2018年05月11日
 * @version   1.0.0
 * @remark:   加载资源与权限的对应关系
 * @explain 实现FilterInvocationSecurityMetadataSource接口也是必须的。 首先,这里从数据库中获取信息。 其中loadResourceDefine方法不是必须的,
 *           这个只是加载所有的资源与权限的对应关系并缓存起来,避免每次获取权限都访问数据库(提高性能),然后getAttributes根据参数(被拦截url)返回权限集合。
 *           这种缓存的实现其实有一个缺点,因为loadResourceDefine方法是放在构造器上调用的,而这个类的实例化只在web服务器启动时调用一次,那就是说loadResourceDefine方法只会调用一次,
 *           如果资源和权限的对应关系在启动后发生了改变,那么缓存起来的权限数据就和实际授权数据不一致,那就会授权错误了。但如果资源和权限对应关系是不会改变的,这种方法性能会好很多。
 *           要想解决 权限数据的一致性 可以直接在getAttributes方法里面调用数据库操作获取权限数据,通过被拦截url获取数据库中的所有权限,封装成Collection<ConfigAttribute>返回就行了。(灵活、简单
 *
 *           器启动加载顺序:1:调用loadResourceDefine()方法  2:调用supports()方法   3:调用getAllConfigAttributes()方法
 *
 *
 */
@Slf4j
@Component
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
    //存放资源配置对象
    private static Map<String, Collection<ConfigAttribute>> resourceMap = null;
    @Autowired
    private ModuleService moduleService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private UrlMatcher urlMatcher;

    /**
     * 参数是要访问的url,返回这个url对于的所有权限(或角色)
     * 每次请求后台就会调用 得到请求所拥有的权限
     * 这个方法在url请求时才会调用,服务器启动时不会执行这个方法
     * getAttributes这个方法会根据你的请求路径去获取这个路径应该是有哪些权限才可以去访问。
     *
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
       // if (resourceMap == null){  //取消这段代码注释 情况下 每次服务启动后请求后台只有到数据库中取一次权限   如果注释掉这段代码则每次请求都会到数据库中取权限
            loadResourceDefine();  // 每次请求 都会去数据库查询权限  貌似很耗性能
       // }
        // object 是一个URL,被用户请求的url。
        String url = ((FilterInvocation) object).getRequestUrl();
        log.info("请求 url :" + url);
        int firstQuestionMarkIndex = url.indexOf("?");
        if (firstQuestionMarkIndex != -1) {
            url = url.substring(0, firstQuestionMarkIndex);
        }
        //循环已有的角色配置对象 进行url匹配
        Iterator<String> ite = resourceMap.keySet().iterator();
        while (ite.hasNext()) {
            String resURL = ite.next().trim();
            if (urlMatcher.pathMatchesUrl(resURL, url)) {     // 路径支持Ant风格的通配符 /spitters/**
                return resourceMap.get(resURL);
            }
           /* if (url.equals(resURL)) {   // 路径不支持Ant风格的通配符
                //返回当前 url  所需要的权限
                return resourceMap.get(resURL);
            }*/
        }
        return null ;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
       //要返回true  不然要报异常   SecurityMetadataSource does not support secure object class: class
        return true;
    }


    /**
     * 初始化资源 ,提取系统中的所有权限,加载所有url和权限(或角色)的对应关系,  web容器启动就会执行
     * 如果启动@PostConstruct 注解   则web容器启动就会执行
     */
    //@PostConstruct
    public void loadResourceDefine() {
       // if (resourceMap == null) {
            //应当是资源为key, 权限为value。 资源通常为url, 权限就是那些以ROLE_为前缀的角色。 一个资源可以由多个权限来访问。
            resourceMap = new ConcurrentHashMap<>();
            //获取所有分配的角色
            List<SysRole> roleList = this.roleService.findByRoleModule();
            //容器启动时,获取全部系统菜单资源信息
            List<SysModuleVO> moduleList = this.moduleService.findByRoleModule();
            if (!CollectionUtils.isEmpty(roleList)){
                for (SysRole role : roleList){
                    //授权标识
                    String authorizedSigns = role.getAuthorizedSigns().trim();
                    ConfigAttribute configAttributes = new SecurityConfig(authorizedSigns);
                    for (SysModuleVO module : moduleList){
                        boolean flag = String.valueOf(role.getId()).equals(module.getAuthorizedSigns());
                        if(flag){
                            //请求url
                            String url =StringUtils.isNotBlank(module.getMenuUrl()) ? module.getMenuUrl().trim() : "";
                            // 判断资源文件和权限的对应关系,如果已经存在相关的资源url,则要通过该url为key提取出权限集合,将权限增加到权限集合中。
                            if (resourceMap.containsKey(url)) {
                                Collection<ConfigAttribute> value = resourceMap.get(url);
                                value.add(configAttributes);
                                resourceMap.put(url, value);
                            } else {
                                Collection<ConfigAttribute> atts = new ArrayList<ConfigAttribute>();
                                atts.add(configAttributes);
                                resourceMap.put(url, atts);
                            }
                        }
                    }
                }
            }

      //  }

    }

}

MyFilterSecurityInterceptor 请求过滤

/***
 *
 * @FileName: MyFilterSecurityInterceptor
 * @Company:
 * @author    
 * @Date      2018年05月11日
 * @version   1.0.0
 * @remark:   过滤用户请求
 * @explain   继承AbstractSecurityInterceptor、实现Filter是必须的
 *             首先,登陆后,每次访问资源都会被这个拦截器拦截,会执行doFilter这个方法,这个方法调用了invoke方法,其中fi断点显示是一个url
 *             最重要的是beforeInvocation这个方法,它首先会调用MyInvocationSecurityMetadataSource类的getAttributes方法获取被拦截url所需的权限
 *             在调用MyAccessDecisionManager类decide方法判断用户是否具有权限,执行完后就会执行下一个拦截器
 *
 *
 */
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    /**
     * 登录后 每次请求都会调用这个拦截器进行请求过滤
     * @param servletRequest
     * @param servletResponse
     * @param filterChain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
        invoke(fi);
    }

    @Override
    public void destroy() {

    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

   /* @Override
    public void setAccessDecisionManager(MyAccessDecisionManager accessDecisionManager) {
        super.setAccessDecisionManager(this.accessDecisionManager);
    }*/
    @Autowired
    public void setAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
        super.setAccessDecisionManager(myAccessDecisionManager);
    }


    /**
     * 拦截请求处理
     * @param fi
     * @throws IOException
     * @throws ServletException
     */
    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        //fi里面有一个被拦截的url
        //里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
        //再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            //执行下一个拦截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }
}

MyUserDetailService 用户登录身份认证


/***
 *
 * @FileName: MyUserDetailService
 * @Company:
 * @author    
 * @Date      2018年05月11日
 * @version   1.0.0
 * @remark:   配置用户权限认证
 * @explain   当用户登录时会进入此类的loadUserByUsername方法对用户进行验证,验证成功后会被保存在当前回话的principal对象中
 *             系统获取当前登录对象信息方法 WebUserDetails webUserDetails = (WebUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
 *
 *              异常信息:
 *              UsernameNotFoundException     用户找不到
 *              BadCredentialsException       坏的凭据
 *              AccountExpiredException       账户过期
 *              LockedException               账户锁定
 *              DisabledException             账户不可用
 *              CredentialsExpiredException   证书过期
 *
 *
 */
@Slf4j
@Service("myUserDetailService")
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private UserRoleService userRoleService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("登录用户:" + username);
        //用户用户信息和用户角色
        UserRoleVO userRole = this.userRoleService.findUserAndRole(username);
        if(userRole.getUserId() == null){
            //后台抛出的异常是:org.springframework.security.authentication.BadCredentialsException: Bad credentials  坏的凭证 如果要抛出UsernameNotFoundException 用户找不到异常则需要自定义重新它的异常
            log.info("登录用户:" + username + " 不存在.");
            throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
        }

        //获取用户信息
        UserInfoVO userInfo = userRole.getUserInfo();
        //获取用户拥有的角色
        List<RoleVO> roleList = userRole.getRoles();
        Set<GrantedAuthority> grantedAuths = new HashSet<GrantedAuthority>();
        if(roleList.size() > 0){
            roleList.stream().forEach(role ->{
                grantedAuths.add(new SimpleGrantedAuthority(role.getAuthorizedSigns()));
            });
        }
        User userDetail = new User(userInfo.getUserAccount(),userInfo.getUserPwd(),
                grantedAuths);

       //不使用jwt 代码
       //return userDetail;


        //使用JWT 代码
        UserDetail user = DozerBeanMapperUtil.copyProperties(userInfo,UserDetail.class);
        user.setUserId(userInfo.getId());
        return JWTUserDetailsFactory.create(userDetail,user);
    }

}

WebSecurityConfig web 安全配置

/***
 *
 * @FileName: WebSecurityConfig
 * @Company:
 * @author    ljy
 * @Date      2018年05月11日
 * @version   1.0.0
 * @remark:   web 安全性配置
 * @explain   当用户登录时会进入此类的loadUserByUsername方法对用户进行验证,验证成功后会被保存在当前回话的principal对象中
 *             系统获取当前登录对象信息方法 WebUserDetails webUserDetails = (WebUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
 *
 */

@Configuration
@EnableWebSecurity  //启动web安全性
@EnableGlobalMethodSecurity(prePostEnabled = true)  //开启方法级的权限注解  性设置后控制器层的方法前的@PreAuthorize("hasRole('admin')") 注解才能起效
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailService myUserDetailService;
    @Autowired
    private MyFilterSecurityInterceptor myFilterSecurityInterceptor;

    @Value("${jwt.route.authentication.path}")
    private String authenticationPath;

    // 不需要认证的接口
    @Value("${com.example.oauth.security.antMatchers}")
    private String antMatchers;

    /**
     * 置user-detail服务
     *
     * 方法描述
     * accountExpired(boolean)                定义账号是否已经过期
     * accountLocked(boolean)                 定义账号是否已经锁定
     * and()                                  用来连接配置
     * authorities(GrantedAuthority...)       授予某个用户一项或多项权限
     * authorities(List)                      授予某个用户一项或多项权限
     * authorities(String...)                 授予某个用户一项或多项权限
     * disabled(boolean)                      定义账号是否已被禁用
     * withUser(String)                       定义用户的用户名
     * password(String)                       定义用户的密码
     * roles(String...)                       授予某个用户一项或多项角色
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //super.configure(auth);
        // 配置指定用户权限信息  通常生产环境都是从数据库中读取用户权限信息而不是在这里配置
        //auth.inMemoryAuthentication().withUser("username1").password("123456").roles("USER").and().withUser("username2").password("123456").roles("USER","AMDIN");

        // ****************   基于数据库中的用户权限信息 进行认证
        //指定密码加密所使用的加密器为 bCryptPasswordEncoder()
        //需要将密码加密后写入数据库
        // myUserDetailService 类中获取了用户的用户名、密码以及是否启用的信息,查询用户所授予的权限,用来进行鉴权,查询用户作为群组成员所授予的权限
        auth.userDetailsService(myUserDetailService).passwordEncoder(bCryptPasswordEncoder());
        //不删除凭据,以便记住用户
        auth.eraseCredentials(false);

    }

    /**
     * 配置Spring Security的Filter链
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        //解决静态资源被拦截的问题
        web.ignoring().antMatchers("/favicon.ico");
        web.ignoring().antMatchers("/error");
        super.configure(web);
    }

    /**
     *  解决 无法直接注入 AuthenticationManager
     * @return
     * @throws Exception
     */
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置如何通过拦截器保护请求
     * 指定哪些请求需要认证,哪些请求不需要认证,以及所需要的权限
     * 通过调用authorizeRequests()和anyRequest().authenticated()就会要求所有进入应用的HTTP请求都要进行认证
     *
     * 方法描述
     * anonymous()                                        允许匿名用户访问
     * authenticated()                                    允许经过认证的用户访问
     * denyAll()                                          无条件拒绝所有访问
     * fullyAuthenticated()                如果用户是完整的话(不是通过Remember-me功能认证的),就允许访问
     * hasAnyAuthority(String...)                 如果用户具备给定权限中的某一个的话,就允许访问
     * hasAnyRole(String...)                    如果用户具备给定角色中的某一个的话,就允许访问
     * hasAuthority(String)                     如果用户具备给定权限的话,就允许访问
     * hasIpAddress(String)                    如果请求来自给定IP地址的话,就允许访问
     * hasRole(String)                        如果用户具备给定角色的话,就允许访问
     * not()                               对其他访问方法的结果求反
     * permitAll()                           无条件允许访问
     * rememberMe()                          如果用户是通过Remember-me功能认证的,就允许访问
     *
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println("不需要认证的url:"+antMatchers);
        //super.configure(http);
        //关闭csrf验证
        http.csrf().disable()
                // 基于token,所以不需要session  如果基于session 则表使用这段代码
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //对请求进行认证  url认证配置顺序为:1.先配置放行不需要认证的 permitAll() 2.然后配置 需要特定权限的 hasRole() 3.最后配置 anyRequest().authenticated()
                .authorizeRequests()
                // 所有 /oauth/v1/api/login/ 请求的都放行 不做认证即不需要登录即可访问
                .antMatchers(antMatchers.split(",")).permitAll()
                //.antMatchers("/auth/v1/api/login/**","/auth/v1/api/module/tree/**","/auth/v1/api/grid/**").permitAll()
                // 对于获取token的rest api要允许匿名访问
                .antMatchers("oauth/**").permitAll()
                // 其他请求都需要进行认证,认证通过够才能访问   待考证:如果使用重定向 httpServletRequest.getRequestDispatcher(url).forward(httpServletRequest,httpServletResponse); 重定向跳转的url不会被拦截(即在这里配置了重定向的url需要特定权限认证不起效),但是如果在Controller 方法上配置了方法级的权限则会进行拦截
                .anyRequest().authenticated()
                .and().exceptionHandling()
                // 认证配置当用户请求了一个受保护的资源,但是用户没有通过登录认证,则抛出登录认证异常,MyAuthenticationEntryPointHandler类中commence()就会调用
                .authenticationEntryPoint(myAuthenticationEntryPoint())
                //用户已经通过了登录认证,在访问一个受保护的资源,但是权限不够,则抛出授权异常,MyAccessDeniedHandler类中handle()就会调用
                .accessDeniedHandler(myAccessDeniedHandler())
                .and()
                //
                .formLogin()
                // 登录url
                .loginProcessingUrl("/auth/v1/api/login/entry")  // 此登录url 和Controller 无关系
               // .loginProcessingUrl("/auth/v1/api/login/enter")  //使用自己定义的Controller 中的方法 登录会进入Controller 中的方法
                // username参数名称 后台接收前端的参数名
                .usernameParameter("userAccount")
                //登录密码参数名称 后台接收前端的参数名
                .passwordParameter("userPwd")
                //登录成功跳转路径
                .successForwardUrl("/")
                //登录失败跳转路径
                .failureUrl("/")
                //登录页面路径
                .loginPage("/")
                .permitAll()
                //登录成功后 MyAuthenticationSuccessHandler类中onAuthenticationSuccess()被调用
                .successHandler(myAuthenticationSuccessHandler())
                //登录失败后 MyAuthenticationFailureHandler 类中onAuthenticationFailure()被调用
                .failureHandler(myAuthenticationFailureHandler())
                .and()
                .logout()
                //退出系统url
                .logoutUrl("/auth/v1/api/login/logout")
                //退出系统后的url跳转
                .logoutSuccessUrl("/")
                //退出系统后的 业务处理
                .logoutSuccessHandler(myLogoutSuccessHandler())
                .permitAll()
                .invalidateHttpSession(true)
                .and()
                //登录后记住用户,下次自动登录,数据库中必须存在名为persistent_logins的表
                // 勾选Remember me登录会在PERSISTENT_LOGINS表中,生成一条记录
                .rememberMe()
                //cookie的有效期(秒为单位
                .tokenValiditySeconds(3600);
        // 加入自定义UsernamePasswordAuthenticationFilter替代原有Filter
        http.addFilterAt(myUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        //在 beforeFilter 之前添加 自定义 filter
        http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
        // 添加JWT filter 验证其他请求的Token是否合法
        http.addFilterBefore(authenticationTokenFilterBean(), FilterSecurityInterceptor.class);
        // 禁用缓存
        http.headers().cacheControl();


    }


    /**
     * 密码加密方式
     * @return
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }


    /**
     * 注册  登录认证 bean
     * @return
     */
    @Bean
    public AuthenticationEntryPoint myAuthenticationEntryPoint(){

        //return new MyAuthenticationEntryPointHandler();
        return new JwtAuthenticationEntryPoint();
    }

    /**
     * 注册  认证权限不足处理 bean
     * @return
     */
    @Bean
    public AccessDeniedHandler myAccessDeniedHandler(){
        return new MyAccessDeniedHandler();
    }

    /**
     * 注册  登录成功 处理 bean
     * @return
     */
    @Bean
    public AuthenticationSuccessHandler myAuthenticationSuccessHandler(){
        return new MyAuthenticationSuccessHandler();
    }

    /**
     *  注册 登录失败 处理 bean
     * @return
     */
    @Bean
    public AuthenticationFailureHandler myAuthenticationFailureHandler(){
        return new MyAuthenticationFailureHandler();
    }

    /**
     * 注册 退出系统成功 处理bean
     * @return
     */
    @Bean
    public LogoutSuccessHandler myLogoutSuccessHandler(){
        return new MyLogoutSuccessHandler();
    }

    /**
     * 注册jwt 认证
     * @return
     * @throws Exception
     */
    @Bean
    public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
        // JwtAuthenticationTokenFilter 过滤器被配置为跳过这个点:/auth/v1/api/login/retrieve/pwd 和 /auth/v1/api/login/entry 不进行token 验证. 通过 SkipPathRequestMatcher 实现 RequestMatcher 接口来实现。
        List<String> pathsToSkip = Arrays.asList("/auth/v1/api/login/retrieve/pwd","/auth/v1/api/login/entry","/auth/v1/api/login/enter");  //不需要token 验证的url
        String processingPath = "/auth/v1/api/**"; // 需要验证token 的url
        SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, processingPath);
        return new JwtAuthenticationTokenFilter(matcher);
    }

    /**
     * 验证登录验证码
     * @return
     * @throws Exception
     */
    @Bean
    public UsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter() throws Exception {
        return new MyUsernamePasswordAuthenticationFilter(authenticationManagerBean(),myAuthenticationSuccessHandler(),myAuthenticationFailureHandler());
    }
}

MyAccessDeniedHandler 自定义权限不足处理类

/***
 *
 * @FileName: MyAccessDeniedHandler
 * @Company:
 * @author    ljy
 * @Date      2018年05月15日
 * @version   1.0.0
 * @remark:   自定义权限不足 需要做的业务操作
 * @explain   当用户登录系统后访问资源时因权限不足则会进入到此类并执行相关业务
 *
 */
@Slf4j
@Component
public class MyAccessDeniedHandler  implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        StringBuffer msg = new StringBuffer("请求: ");
        msg.append(httpServletRequest.getRequestURI()).append(" 权限不足,无法访问系统资源.");
        log.info(msg.toString());
        ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.AUTHORITY,msg.toString());


       /* boolean ajaxRequest = HttpUtils.isAjaxRequest(httpServletRequest);
        if (ajaxRequest){
            //如果是ajax请求 则返回403错
            ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.AUTHORITY,msg.toString());
        }else {
            // 非ajax请求 则跳转到指定的403页面
            //此处省略...................
        }*/
    }
}

MyAuthenticationEntryPointHandler 认证失败处理类

/***
 *
 * @FileName: MyAuthenticationEntryPointHandler
 * @Company:
 * @author    ljy
 * @Date      2018年05月15日
 * @version   1.0.0
 * @remark:   认证失败 需要做的业务操作
 * @explain   当检测到用户访问系统资源认证失败时则会进入到此类并执行相关业务
 *
 */
@Slf4j
@Component
public class MyAuthenticationEntryPointHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        StringBuffer msg = new StringBuffer("请求访问: ");
        msg.append(httpServletRequest.getRequestURI()).append(" 接口, 因为登录超时,无法访问系统资源.");
        log.info(msg.toString());
        ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.LOGIN_WITHOUT,msg.toString());

      /*  boolean ajaxRequest = HttpUtils.isAjaxRequest(httpServletRequest);
        if (ajaxRequest){
            //如果是ajax请求 则返回自定义错误
            ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.LOGIN,map);
        }else {
            // 非ajax请求 则跳转到指定的403页面
            //此处省略...................
        }*/
    }
}

MyAuthenticationException 异常

public class MyAuthenticationException extends AuthenticationException {

    public MyAuthenticationException(String msg, Throwable t) {
        super(msg, t);
    }

    public MyAuthenticationException(String msg) {
        super(msg);
    }

    /**
     * 加入错误状态值
     * @param exceptionEnum
     */
    public MyAuthenticationException(ErrorCodeEnum exceptionEnum) {
        super(exceptionEnum.getMessage());
    }

}

MyAuthenticationFailureHandler 登录失败处理类

/***
 *
 * @FileName: MyAuthenticationFailureHandler
 * @Company:
 * @author    ljy
 * @Date      2018年05月15日
 * @version   1.0.0
 * @remark:   用户登录系统失败后 需要做的业务操作
 * @explain   当用户登录系统失败后则会进入到此类并执行相关业务
 *
 */
@Slf4j
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        //用户登录时身份认证未通过
        if (e instanceof BadCredentialsException){
            log.info("用户登录时:用户名或者密码错误.");
            ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.LOGIN_INCORRECT);
        }else{
            ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.LOGIN_FAIL);
        }
    }
}

MyAuthenticationSuccessHandler 登录成功处理类

/***
 *
 * @FileName: MyAuthenticationSuccessHandler
 * @Company:
 * @author    ljy
 * @Date      2018年05月15日
 * @version   1.0.0
 * @remark:   用户登录系统成功后 需要做的业务操作
 * @explain   当用户登录系统成功后则会进入到此类并执行相关业务
 *
 */
@Slf4j
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private RedisUtil redisUtil;
    @Value("${jwt.header}")
    private String tokenHeader;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Value("${jwt.expiration}")
    private Long expiration;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //获得授权后可得到用户信息(非jwt 方式)
        //User userDetails =  (User) authentication.getPrincipal();

        //获得授权后可得到用户信息(jwt 方式)
        JWTUserDetails userDetails =  (JWTUserDetails) authentication.getPrincipal();
        //将身份 存储到SecurityContext里
        SecurityContext securityContext = SecurityContextHolder.getContext();
        securityContext.setAuthentication(authentication);
        httpServletRequest.getSession().setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
        StringBuffer msg = new StringBuffer("用户:");
        msg.append(userDetails.getUsername()).append(" 成功登录系统.");
        log.info(msg.toString());
        //使用jwt生成token 用于权限效验
        String token = jwtTokenUtil.generateAccessToken(userDetails);
        UserDetail user = userDetails.getUserInfo();
        user.setToken(token);
        //将登录人信息放在redis中
        this.saveTokenToRedis(user.getAccountId(),token,JSON.toJSONString(user));
        String access_token = tokenHead+token;
        String refresh_token = tokenHead+jwtTokenUtil.refreshToken(token);
        Map<String,String> map = new HashMap<>();
        map.put("access_token", access_token);
        map.put("refresh_token", refresh_token);
        map.put("userId",user.getAccountId().toString());
        map.put("userName",user.getUserName());
        map.put("email",user.getUserEmail());
        map.put("msage",msg.toString());
        RestfulVo restfulVo = ResultUtil.resultInfo(ErrorCodeEnum.SUCCESS,map);
        ResultUtil.writeJavaScript(httpServletResponse,restfulVo);
    }

    /**
     * 将用户token 和用户信息 保存到redis中
     * @param userId  用户id
     * @param token   用户token
     * @param value   用户信息
     */
    private void saveTokenToRedis(Long userId,String token,String value){
        String userKey =  RedisKeys.USER_KEY;
        redisUtil.hset(userKey,token,value,expiration);
    }
}

MyLogoutSuccessHandler 退出成功后处理类

/***
 *
 * @FileName: MyLogoutSuccessHandler
 * @Company:
 * @author    ljy
 * @Date      2018年05月15日
 * @version   1.0.0
 * @remark:   用户退出系统成功后 需要做的业务操作
 * @explain   当用户退出系统成功后则会进入到此类并执行相关业务
 *
 */
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

    @Autowired
    private RedisUtil redisUtil;
   @Autowired
    private UserUtils userUtils;

    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //根据token清空redis
        String userKey =  RedisKeys.USER_KEY;
        String token = userUtils.getUserToken(httpServletRequest);
        redisUtil.hdel(userKey,token);
        SecurityContextHolder.clearContext();  //清空上下文
        httpServletRequest.getSession().removeAttribute("SPRING_SECURITY_CONTEXT"); // 从session中移除
        //退出信息插入日志记录表中
        ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.SUCCESS,"退出系统成功.");
    }
}

MyUsernamePasswordAuthenticationFilter 校验验证码

/***
 *
 * @FileName: MyUsernamePasswordAuthenticationFilter
 * @Company:
 * @author    ljy
 * @Date      2018年07月15日
 * @version   1.0.0
 * @remark:   自定义 登录校验
 * @explain   调用登录接口时会进入到此类的attemptAuthentication方法 进行相关校验操作
 *
 */
@Slf4j
public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public  MyUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager,AuthenticationSuccessHandler successHandler,AuthenticationFailureHandler failureHandler){
        this.setFilterProcessesUrl("/auth/v1/api/login/entry");  //这句代码很重要,设置登陆的url 要和 WebSecurityConfig 配置类中的.loginProcessingUrl("/auth/v1/api/login/entry") 一致,如果不配置则无法执行 重写的attemptAuthentication 方法里面而是执行了父类UsernamePasswordAuthenticationFilter的attemptAuthentication()
        this.setAuthenticationManager(authenticationManager);   // AuthenticationManager 是必须的
        this.setAuthenticationSuccessHandler(successHandler);  //设置自定义登陆成功后的业务处理
        this.setAuthenticationFailureHandler(failureHandler); //设置自定义登陆失败后的业务处理
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //校验验证码
        String verifyCode = request.getParameter("verifyCode");
        if(!checkValidateCode(verifyCode)){
            ResultUtil.writeJavaScript(response,ErrorCodeEnum.FAIL,"验证码错误.");
            return null;
        }
        //设置获取 username 的属性字段   js传到后台接收数据的参数名
        this.setUsernameParameter("userAccount");
        //设置获取password 的属性字段  js传到后台接收数据的参数名
        this.setPasswordParameter("userPwd");

        return super.attemptAuthentication(request, response);
    }

    /**
     * 验证 验证码是否正确
     * @param verifyCode
     * @return
     */
    private boolean checkValidateCode(String verifyCode){
        if(StringUtils.isBlank(verifyCode) || !verifyCode.trim().equals("1234")){
          return false;
        }
        return true;
    }
}

整合JWT

JwtTokenUtil jwt 工具类

/***
 *
 * @FileName: JwtTokenUtil
 * @Company:
 * @author    ljy
 * @Date      2018年05月12日
 * @version   1.0.0
 * @remark:   jwt工具类  提供校验toeken 、生成token、根据token获取用户等方法
 *
 */
@Component
public class JwtTokenUtil implements Serializable {

    private static final long serialVersionUID = -5883980282405596071L;

    //
    public static final String ROLE_REFRESH_TOKEN = "ROLE_REFRESH_TOKEN";
    private static final String CLAIM_KEY_USER_ID = "user_id";
    private static final String CLAIM_KEY_AUTHORITIES = "scope";
    private static final String CLAIM_KEY_ACCOUNT_ENABLED = "enabled";
    private static final String CLAIM_KEY_ACCOUNT_NON_LOCKED = "non_locked";
    private static final String CLAIM_KEY_ACCOUNT_NON_EXPIRED = "non_expired";
    private static final String CLAIM_KEY_USER_ACCOUNT = "sub";
    private static final String CLAIM_KEY_CREATED = "created";
    //签名方式
    private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;

    //密匙
    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.access_token}")
    private Long access_token_expiration;

    @Value("${jwt.refresh_token}")
    private Long refresh_token_expiration;
    //过期时间
    @Value("${jwt.expiration}")
    private Long expiration;

    /**
     * 根据token 获取用户信息
     * @param token
     * @return
     */
    public JWTUserDetails getUserFromToken(String token) {
        JWTUserDetails  jwtUserDetails;
        try {
            final Claims claims = getClaimsFromToken(token);
            long userId = getUserIdFromToken(token);
            String username = claims.getSubject();
            List<?> roles = (List<?>) claims.get(CLAIM_KEY_AUTHORITIES);
            Collection<? extends GrantedAuthority> authorities = parseArrayToAuthorities(roles);
            boolean account_enabled = (Boolean) claims.get(CLAIM_KEY_ACCOUNT_ENABLED);
            boolean account_non_locked = (Boolean) claims.get(CLAIM_KEY_ACCOUNT_NON_LOCKED);
            boolean account_non_expired = (Boolean) claims.get(CLAIM_KEY_ACCOUNT_NON_EXPIRED);
            User user = new User(username, "", account_enabled, account_non_expired, account_non_expired, account_non_locked, authorities);

            jwtUserDetails = JWTUserDetailsFactory.create(user, userId,Instant.now());
        } catch (Exception e) {
            jwtUserDetails = null;
        }
        return jwtUserDetails;
    }

    /**
     * 根据token 获取用户ID
     * @param token
     * @return
     */
    public long getUserIdFromToken(String token) {
        long userId;
        try {
            final Claims claims = getClaimsFromToken(token);
            userId = Long.valueOf(claims != null ? claims.get(CLAIM_KEY_USER_ID).toString() :"0");
        } catch (Exception e) {
            e.printStackTrace();
            userId = 0;
        }
        return userId;
    }

    /**
     * 根据token 获取用户名
     * @param token
     * @return
     */
    public String getUsernameFromToken(String token) {
        String username;
        try {
            final Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 根据token 获取生成时间
     * @param token
     * @return
     */
    public Date getCreatedDateFromToken(String token) {
        Date created;
        try {
            final Claims claims = getClaimsFromToken(token);
            created = claims.getIssuedAt();
        } catch (Exception e) {
            created = null;
        }
        return created;
    }

    /**
     * 根据token 获取过期时间
     * @param token
     * @return
     */
    public Date getExpirationDateFromToken(String token) {
        Date expiration;
        try {
            final Claims claims = getClaimsFromToken(token);
            expiration = claims.getExpiration();
        } catch (Exception e) {
            expiration = null;
        }
        return expiration;
    }

    /***
     * 解析token 信息
     * @param token
     * @return
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)   //签名的key
                    .parseClaimsJws(token)   // 签名token
                    .getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 生成失效时间
     * @param expiration
     * @return
     */
    private Date generateExpirationDate(long expiration) {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * token 是否过期
     * @param token
     * @return
     */
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    /**
     * 生成时间是否在最后修改时间之前
     * @param created   生成时间
     * @param lastPasswordReset  最后修改密码时间
     * @return
     */
    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        return (lastPasswordReset != null && created.before(lastPasswordReset));
    }

    /**
     * 根据用户信息 生成token
     * @param userDetails
     * @return
     */
    public String generateAccessToken(UserDetails userDetails) {
        JWTUserDetails user = (JWTUserDetails) userDetails;
        Map<String, Object> claims = generateClaims(user);
        claims.put(CLAIM_KEY_AUTHORITIES, JSON.toJSON(authoritiesToArray(user.getAuthorities())));
        return generateAccessToken(user.getUsername(), claims);
    }

    /**
     * 重置(更新)token 过期时间
     * @param token
     * @param expiration
     */
    public String restTokenExpired(String token,long expiration){

        final Claims claims = getClaimsFromToken(token);
        Jwts.builder()
                .setClaims(claims)   //一个map 可以资源存放东西进去
                .setSubject(claims.getSubject()) //  用户名写入标题
                .setExpiration(new Date(expiration));
        //claims.setExpiration(new Date(expiration));
        // String refreshedToken = generateAccessToken(claims.getSubject(), claims,expiration);
        return "";
    }

    private Map<String, Object> generateClaims(JWTUserDetails user) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USER_ID, user.getUserId());
        claims.put(CLAIM_KEY_ACCOUNT_ENABLED, user.isEnabled());
        claims.put(CLAIM_KEY_ACCOUNT_NON_LOCKED, user.isAccountNonLocked());
        claims.put(CLAIM_KEY_ACCOUNT_NON_EXPIRED, user.isAccountNonExpired());
        return claims;
    }

    /**
     * 生成token
     * @param subject  用户名
     * @param claims
     * @return
     */
    private String generateAccessToken(String subject, Map<String, Object> claims) {
        return generateToken(subject, claims, access_token_expiration);
    }


    /**
     * 生成token
     * @param subject  用户名
     * @param claims
     * @return
     */
    private String generateAccessToken(String subject, Map<String, Object> claims,long expiration) {
        return generateToken(subject, claims, expiration);
    }

    /**
     * 用户所拥有的资源权限
     * @param authorities
     * @return
     */
    private List<?> authoritiesToArray(Collection<? extends GrantedAuthority> authorities) {
        List<String> list = new ArrayList<>();
        for (GrantedAuthority ga : authorities) {
            list.add(ga.getAuthority());
        }
        return list;
    }

    private Collection<? extends GrantedAuthority> parseArrayToAuthorities(List<?> roles) {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        SimpleGrantedAuthority authority;
        for (Object role : roles) {
            authority = new SimpleGrantedAuthority(role.toString());
            authorities.add(authority);
        }
        return authorities;
    }

    /**
     * 根据用户信息 重新获取token
     * @param userDetails
     * @return
     */
    public String generateRefreshToken(UserDetails userDetails) {
        JWTUserDetails user = (JWTUserDetails) userDetails;
        Map<String, Object> claims = generateClaims(user);
        // 只授于更新 token 的权限
        String roles[] = new String[]{ROLE_REFRESH_TOKEN};
        claims.put(CLAIM_KEY_AUTHORITIES, JSON.toJSON(roles));
        return generateRefreshToken(user.getUsername(), claims);
    }

    /**
     * 重新获取token
     * @param subject 用户名
     * @param claims
     * @return
     */
    private String generateRefreshToken(String subject, Map<String, Object> claims) {
        return generateToken(subject, claims, refresh_token_expiration);
    }

    public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
        final Date created = getCreatedDateFromToken(token);
        return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
                && (!isTokenExpired(token));
    }

    /**
     * 刷新重新获取token
     * @param token 源token
     * @return
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            final Claims claims = getClaimsFromToken(token);
            refreshedToken = generateAccessToken(claims.getSubject(), claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    private String generateToken(String subject, Map<String, Object> claims, long expiration) {
        return Jwts.builder()
                .setClaims(claims)   //一个map 可以资源存放东西进去
                .setSubject(subject) //  用户名写入标题
                .setId(UUID.randomUUID().toString())
                .setIssuedAt(new Date())
                .setExpiration(generateExpirationDate(expiration))  //过期时间
                //.setNotBefore(now)              //系统时间之前的token都是不可以被承认的
                .signWith(SIGNATURE_ALGORITHM, secret) //数字签名
                .compact();
    }

    /**
     * 验证token 是否合法
     * @param token  token
     * @param userDetails  用户信息
     * @return
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        JWTUserDetails user = (JWTUserDetails) userDetails;
        final long userId = getUserIdFromToken(token);
        final String username = getUsernameFromToken(token);
        // final Date created = getCreatedDateFromToken(token);
        // final Date expiration = getExpirationDateFromToken(token);
        return (userId == user.getUserId()
                && username.equals(user.getUsername())
                && !isTokenExpired(token)
                /* && !isCreatedBeforeLastPasswordReset(created, userDetails.getLastPasswordResetDate()) */
        );
    }


}

JWTUserDetails 用户信息

/***
 *
 * @FileName: JWTUserDetails
 * @Company:
 * @author    ljy
 * @Date      2018年05月120日
 * @version   1.0.0
 * @remark:   jwt用户信息
 * @explain   Spring Security需要我们实现几个东西,第一个是UserDetails:这个接口中规定了用户的几个必须要有的方法,所以我们创建一个JwtUser类来实现这个接口。为什么不直接使用User类?因为这个UserDetails完全是为了安全服务的,它和我们的领域类可能有部分属性重叠,但很多的接口其实是安全定制的,所以最好新建一个类:
 *
 */
public class JWTUserDetails implements UserDetails {

    private Long userId;         //用户ID
    private String password;       //用户密码
    private final String username; //用户名
    private final Collection<? extends GrantedAuthority> authorities;  //用户角色权限
    private final Boolean isAccountNonExpired;       //账号是否过期
    private final Boolean isAccountNonLocked;        //账户是否锁定
    private final Boolean isCredentialsNonExpired;   //密码是否过期
    private  Boolean enabled;                   //是否激活
    private final Instant lastPasswordResetDate;        //上次密码重置时间
    private UserDetail userInfo;

    public JWTUserDetails(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities,Instant lastPasswordResetDate) {
        this(userId, username, password, true, true, true, true, authorities,lastPasswordResetDate);
    }

    public JWTUserDetails(UserDetail userInfo, Collection<? extends GrantedAuthority> authorities) {
        this.userInfo = userInfo;
        if (userInfo != null && StringUtils.isNotBlank(userInfo.getUserAccount())) {
            this.userId = userInfo.getAccountId();
            this.username = userInfo.getUserAccount();
            this.password = userInfo.getUserPwd();
            this.enabled = userInfo.getStatus() == 0 ? false : true;
            this.isAccountNonExpired = true;
            this.isAccountNonLocked = true;
            this.isCredentialsNonExpired = true;
            this.authorities = authorities;
            this.lastPasswordResetDate = userInfo.getLastPasswordResetDate();
        } else {
            throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
        }
    }

    public JWTUserDetails(Long userId, String username, String password, boolean enabled, boolean isAccountNonExpired, boolean isCredentialsNonExpired, boolean isAccountNonLocked, Collection<? extends GrantedAuthority> authorities,Instant lastPasswordResetDate) {
        if (username != null && !"".equals(username) && password != null) {
            this.userId = userId;
            this.username = username;
            this.password = password;
            this.enabled = enabled;
            this.isAccountNonExpired = isAccountNonExpired;
            this.isAccountNonLocked = isAccountNonLocked;
            this.isCredentialsNonExpired = isCredentialsNonExpired;
            this.authorities = authorities;
            this.lastPasswordResetDate = lastPasswordResetDate;
        } else {
            throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
        }
    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @JsonIgnore
    @Override
    public String getPassword() {
        return password;
    }


    @Override
    public String getUsername() {
        return username;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return isAccountNonExpired;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return isAccountNonLocked;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return isCredentialsNonExpired;
    }

    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return enabled;
    }

    @JsonIgnore
    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    @JsonIgnore
    public Instant getLastPasswordResetDate() {
        return lastPasswordResetDate;
    }

    public UserDetail getUserInfo() {
        return userInfo;
    }

    public void setUserInfo(UserDetail userInfo) {
        this.userInfo = userInfo;
    }
}

JWTUserDetailsFactory

/***
 *
 * @FileName: JWTUserDetailsFactory
 * @Company:
 * @author    ljy
 * @Date      2018年05月120日
 * @version   1.0.0
 * @remark:   负责创建JWTUserDetails 对象
 *
 */
public final class JWTUserDetailsFactory {

    private JWTUserDetailsFactory(){

    }

    public static JWTUserDetails create(User user, Long userId, Instant date){
        return new JWTUserDetails(userId, user.getUsername(), user.getPassword(),user.getAuthorities(), date);
    }

    public static JWTUserDetails create(User user, UserDetail userDetail){
        return new JWTUserDetails(userDetail,user.getAuthorities());
    }

    private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
        return authorities.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

}

JwtAuthenticationTokenFilter 请求接口时校验token信息

/***
 *
 * @FileName: JwtAuthenticationTokenFilter
 * @Company:
 * @author    ljy
 * @Date      2018年05月120日
 * @version   1.0.0
 * @remark:   jwt认证token
 * @explain   每次请求接口时 就会进入这里验证token 是否合法
 *             token 如果用户一直在操作,则token 过期时间会叠加    如果超过设置的过期时间未操作  则token 失效 需要重新登录
 *
 */
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {


    @Resource(name = "myUserDetailService")
    private UserDetailsService userDetailsService;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.header}")
    private String tokenHeader;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Value("${jwt.expiration}")
    private Long expiration;

    private RequestMatcher authenticationRequestMatcher;

    public JwtAuthenticationTokenFilter() {

    }

    public JwtAuthenticationTokenFilter(RequestMatcher authenticationRequestMatcher) {
        this.authenticationRequestMatcher = authenticationRequestMatcher;
    }


    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        //过滤掉不需要token验证的url
        if(authenticationRequestMatcher != null && !authenticationRequestMatcher.matches(httpServletRequest)){
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }
        String authHeader = httpServletRequest.getHeader(this.tokenHeader);
        if (authHeader != null && authHeader.startsWith(tokenHead)) {
            final String authToken = authHeader.substring(tokenHead.length()); // The part after "Bearer "
            log.info("请求"+httpServletRequest.getRequestURI()+"携带的token值:" + authToken);
            // 查看redis中的token信息是否过期
            boolean isExists = redisUtil.hexists(RedisKeys.USER_KEY,authToken);
            if (!isExists){
                // token 过期 提示用户登录超时 重新登录系统
                //throw  new MyAuthenticationException(ErrorCodeEnum.LOGIN_WITHOUT);
            }

            //如果在token过期之前触发接口,我们更新token过期时间,token值不变只更新过期时间
            Date createTokenDate = jwtTokenUtil.getCreatedDateFromToken(authToken);  //获取token生成时间
            log.info("createTokenDate: " + createTokenDate);
            if(createTokenDate != null){
                Duration between = Duration.between(Instant.now(), Instant.ofEpochMilli(createTokenDate.getTime()));
                Long differSeconds = between.toMillis();
                boolean isExpire = expiration > differSeconds;
                if (isExpire) {  //如果 请求接口时间在token 过期之前 则更新token过期时间  我们可以将用户的token 存放到redis 中,更新redis 的过期时间

                    //更新 延长 redis 中的过期时间


                }
            }


            String useraccount = jwtTokenUtil.getUsernameFromToken(authToken);
            log.info("JwtAuthenticationTokenFilter[doFilterInternal] checking authentication " + useraccount);

            //token校验通过
            if(useraccount != null){
                TokenUtils.setToken(authToken);//设置token
                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                if(authentication == null  || authentication.getPrincipal().equals("anonymousUser")){
                    //根据account去数据库中查询user数据,足够信任token的情况下,可以省略这一步
                    UserDetails userDetails = this.userDetailsService.loadUserByUsername(useraccount);

                    if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                        UsernamePasswordAuthenticationToken usernamePasswordAuthentication = new UsernamePasswordAuthenticationToken(
                                userDetails, null, userDetails.getAuthorities());
                        usernamePasswordAuthentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
                                httpServletRequest));
                        log.info("JwtAuthenticationTokenFilter[doFilterInternal]  authenticated user " + useraccount + ", setting security context");
                        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthentication);
                    }
                }else {
                    log.info("当前请求用户信息:"+ JSON.toJSONString(authentication.getPrincipal()));
                }
            }else {
                log.info("token 无效.");
                throw  new MyAuthenticationException(ErrorCodeEnum.TOKEN_INVALID);
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

JwtAuthenticationEntryPoint jwt认证失败处理

/***
 *
 * @FileName: JwtAuthenticationEntryPoint
 * @Company:
 * @author    ljy
 * @Date      2018年05月120日
 * @version   1.0.0
 * @remark:   jwt 认证处理类
 *
 */
@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        StringBuffer msg = new StringBuffer("请求访问: ");
        msg.append(httpServletRequest.getRequestURI()).append(" 接口, 经jwt 认证失败,无法访问系统资源.");
        log.info(msg.toString());
        log.info(e.toString());
        // 用户登录时身份认证未通过
        if(e instanceof BadCredentialsException) {
            log.info("用户登录时身份认证失败.");
            ResultUtil.writeJavaScript(httpServletResponse, ErrorCodeEnum.LOGIN_INCORRECT, msg.toString());
        }else if(e instanceof InsufficientAuthenticationException){
            log.info("缺少请求头参数,Authorization传递是token值所以参数是必须的.");
            ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.NO_TOKEN,msg.toString());
        }else {
            log.info("用户token无效.");
            ResultUtil.writeJavaScript(httpServletResponse,ErrorCodeEnum.TOKEN_INVALID,msg.toString());
        }

    }
}

工具类

SkipPathRequestMatcher 忽略不进行token校验的url

public class SkipPathRequestMatcher implements RequestMatcher {
    private OrRequestMatcher matchers;
    private RequestMatcher processingMatcher;

    public SkipPathRequestMatcher(List<String> pathsToSkip, String processingPath) {
        List<RequestMatcher> m = pathsToSkip.stream().map(path -> new AntPathRequestMatcher(path)).collect(Collectors.toList());
        matchers = new OrRequestMatcher(m);
        processingMatcher = new AntPathRequestMatcher(processingPath);
    }

    @Override
    public boolean matches(HttpServletRequest request) {
        if (matchers.matches(request)) {
            return false;
        }
        return processingMatcher.matches(request) ? true : false;
    }
}

UrlMatcher url匹配接口

public interface UrlMatcher {
    Object compile(String paramString);
    boolean pathMatchesUrl(Object paramObject, String paramString);
    String getUniversalMatchPattern();
    boolean requiresLowerCaseUrl();
}

AntUrlPathMatcher url 匹配 在 MyInvocationSecurityMetadataSourceService 类中会用到

@Component
public class AntUrlPathMatcher implements UrlMatcher {
    private boolean requiresLowerCaseUrl;
    private PathMatcher pathMatcher;
    public AntUrlPathMatcher()   {
        this(true);

    }
    public AntUrlPathMatcher(boolean requiresLowerCaseUrl)
    {
        this.requiresLowerCaseUrl = true;
        this.pathMatcher = new AntPathMatcher();
        this.requiresLowerCaseUrl = requiresLowerCaseUrl;
    }

    public Object compile(String path) {
        if (this.requiresLowerCaseUrl) {
            return path.toLowerCase();
        }
        return path;
    }

    public void setRequiresLowerCaseUrl(boolean requiresLowerCaseUrl){

        this.requiresLowerCaseUrl = requiresLowerCaseUrl;
    }

    public boolean pathMatchesUrl(Object path, String url) {
        if (("/**".equals(path)) || ("**".equals(path))) {
            return true;
        }

        return this.pathMatcher.match((String)path, url);
    }

    public String getUniversalMatchPattern() {
        return"/**";
    }

    public boolean requiresLowerCaseUrl() {
        return this.requiresLowerCaseUrl;
    }

    public String toString() {
        return super.getClass().getName() + "[requiresLowerCase='"
                + this.requiresLowerCaseUrl + "']";
    }

前端使用ajax方式登录

 //登录页面登录按钮事件
    var handleSignInFormSubmit = function() {
        $('#m_login_signin_submit').click(function(e) {
            e.preventDefault();
            var btn = $(this);
            var form = $(this).closest('form');

            form.validate({
                rules: {
                    userAccount: {
                        required: true
                    },
                    userPwd: {
                        required: true
                    }
                },
                messages: {
                        userAccount: {
                                required: "请输入登录用户."
                        },
                        userPwd: {
                                required: "请输入登录密码."
                        }
                    }
            });

            if (!form.valid()) {
                return;
            }

            btn.addClass('m-loader m-loader--right m-loader--light').attr('disabled', true);

            form.ajaxSubmit({
                type: 'post',
                url: ajaxUrl+'login/entry',
                success: function(response, status, xhr, $form) {
                    console.log(response);
                    if(response.status != 0){
                        btn.removeClass('m-loader m-loader--right m-loader--light').attr('disabled', false);
                        showErrorMsg(form, 'danger', '错误的用户名或密码.');
                    }else {
                        //得到登录后的token
                        var access_token = response.data.access_token;
                        localStorage.setItem('user_token', JSON.stringify(access_token));
                        localStorage.setItem('user',  JSON.stringify(response.data));
                        window.location.href="assets/snippets/pages/home/index.html";
                    }
                },
                error:function (response, status, xhr) {
                    btn.removeClass('m-loader m-loader--right m-loader--light').attr('disabled', false);
                    showErrorMsg(form, 'danger', '网络出现错误.');
                }
            });
            return false; // 阻止表单自动提交事件,必须返回false,否则表单会自己再做一次提交操作,并且页面跳转
        });
    }

携带token信息请求其它接口获取数据(以删除为例)

  var userToken = JSON.parse(localStorage.getItem('user_token'));

  $.ajax({
                url: ajaxUrl+'userRole/del',
                data : {
                    roleId:checkboxRoleId,
                    userIds:selectDeleteUserIds.toString(),
                    _method: 'DELETE'
                },
                type:"POST",
                dataType:"json",
                headers: {'Authorization': userToken},  //携带token到后台的方式
                success: function(response, status, xhr) {
                    var serverStatus = response.status;
                    toastr.clear();
                    //这里根据后台返回的状态做相应的处理,后台会根据token去校验权限,并返回相应的响应
                    if(serverStatus == -1){  
                        //登陆超时,需要重新登陆系统
                        toastr.error(response.message);
                        toastr.info("即将跳转到登陆页面.");
                        window.location.href="../../../../login.html";
                    }else  if (response.status != 0){
                        // 我设置的 response.status == 0 表示成功
                       //  response.status != 0 有可能是 无权限访问、token错误、登录失效等 根据后台认证返回 
                        toastr.error(response.message);
                    }else {
                        reloadUserGrid();
                    }
                    //移除遮罩层
                    mApp.unblock('#be-authorized-userGrid');
                },
                error:function (response, status, xhr) {
                    toastr.error("网络出现错误.");
                }
            });
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容