关于后端:厉害我带的实习生仅用四步就整合好SpringSecurityJWT实现登录认证

3次阅读

共计 7741 个字符,预计需要花费 20 分钟才能阅读完成。

小二是新来的实习生,作为技术 leader,我还是很负责任的,有什么锅都想甩给他,啊,不,一不小心怎么把心里话全说进去了呢?重来!

小二是新来的实习生,作为技术 leader,我还是很负责任的,有什么坏事都想着他,这不,我就安顿了一个整合 SpringSecurity+JWT 实现登录认证的小工作交,没想到,他仅用四步就搞定了,这让我感觉倍有面。
一、对于 SpringSecurity
在 Spring Boot 呈现之前,SpringSecurity 的应用场景是被另外一个平安治理框架 Shiro 牢牢霸占的,因为绝对于 SpringSecurity 来说,SSM 中整合 Shiro 更加轻量级。Spring Boot 呈现后,使这一状况状况大有改观。正应了那句古话:一人得道鸡犬升天,尽管有点不大适合,就将就着用吧。
这是因为 Spring Boot 为 SpringSecurity 提供了自动化配置,大大降低了 SpringSecurity 的学习老本。另外,SpringSecurity 的性能也比 Shiro 更加弱小。

二、对于 JWT
JWT,是目前最风行的一个跨域认证解决方案:客户端发动用户登录申请,服务器端接管并认证胜利后,生成一个 JSON 对象(如下所示),而后将其返回给客户端。

从实质上来说,JWT 就像是一种生成加密用户身份信息的 Token,更平安也更灵便。
三、整合步骤
第一步,给须要登录认证的模块增加 codingmore-security 依赖:
<dependency>

<groupId>top.codingmore</groupId>
<artifactId>codingmore-security</artifactId>
<version>1.0-SNAPSHOT</version>

</dependency>
复制代码
比如说 codingmore-admin 后端治理模块须要登录认证,就在 codingmore-admin/pom.xml 文件中增加 codingmore-security 依赖。

第二步,在须要登录认证的模块里增加 CodingmoreSecurityConfig 类,继承自 codingmore-security 模块中的 SecurityConfig 类。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CodingmoreSecurityConfig extends SecurityConfig {

@Autowired
private IUsersService usersService;

@Bean
public UserDetailsService userDetailsService() {
    // 获取登录用户信息
    return username -> usersService.loadUserByUsername(username);
}

}
复制代码
UserDetailsService 这个类次要是用来加载用户信息的,包含用户名、明码、权限、角色汇合 …. 其中有一个办法如下:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
复制代码
认证逻辑中,SpringSecurity 会调用这个办法依据客户端传入的用户名加载该用户的详细信息,包含判断:

明码是否统一
通过后获取权限和角色

public UserDetails loadUserByUsername(String username) {
    // 依据用户名查问用户
    Users admin = getAdminByUsername(username);
    if (admin != null) {List<Resource> resourceList = getResourceList(admin.getId());
        return new AdminUserDetails(admin,resourceList);
    }
    throw new UsernameNotFoundException("用户名或明码谬误");
}

复制代码
getAdminByUsername 负责依据用户名从数据库中查问出明码、角色、权限等。

public Users getAdminByUsername(String username) {QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("user_login", username);
    List<Users> usersList = baseMapper.selectList(queryWrapper);

    if (usersList != null && usersList.size() > 0) {return usersList.get(0);
    }

    // 用户名谬误,提前抛出异样
    throw new UsernameNotFoundException("用户名谬误");
}

复制代码
第三步,在 application.yml 中配置下不须要平安爱护的资源门路:
secure:
ignored:

urls: #平安门路白名单
  - /doc.html
  - /swagger-ui/**
  - /swagger/**
  - /swagger-resources/**
  - /**/v3/api-docs
  - /**/*.js
  - /**/*.css
  - /**/*.png
  - /**/*.ico
  - /webjars/springfox-swagger-ui/**
  - /actuator/**
  - /druid/**
  - /users/login
  - /users/register
  - /users/info
  - /users/logout

复制代码
第四步,在登录接口中增加登录和刷新 token 的办法:
@Controller
@Api(tags = “ 用户 ”)
@RequestMapping(“/users”)
public class UsersController {

@Autowired
private IUsersService usersService;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;

@ApiOperation(value = “ 登录当前返回 token”)

@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public ResultObject login(@Validated UsersLoginParam users, BindingResult result) {String token = usersService.login(users.getUserLogin(), users.getUserPass());

    if (token == null) {return ResultObject.validateFailed("用户名或明码谬误");
    }

    // 将 JWT 传递回客户端
    Map<String, String> tokenMap = new HashMap<>();
    tokenMap.put("token", token);
    tokenMap.put("tokenHead", tokenHead);
    return ResultObject.success(tokenMap);
}

@ApiOperation(value = "刷新 token")
@RequestMapping(value = "/refreshToken", method = RequestMethod.GET)
@ResponseBody
public ResultObject refreshToken(HttpServletRequest request) {String token = request.getHeader(tokenHeader);
    String refreshToken = usersService.refreshToken(token);
    if (refreshToken == null) {return ResultObject.failed("token 曾经过期!");
    }
    Map<String, String> tokenMap = new HashMap<>();
    tokenMap.put("token", refreshToken);
    tokenMap.put("tokenHead", tokenHead);
    return ResultObject.success(tokenMap);
}

}
复制代码
应用 Apipost 来测试一下,首先是文章获取接口,在没有登录的状况下会提醒暂未登录或者 token 已过期。

四、实现原理
小二之所以能仅用四步就实现了登录认证,次要是因为他将 SpringSecurity+JWT 的代码封装成了通用模块,咱们来看看 codingmore-security 的目录构造。
codingmore-security
├── component
| ├── JwtAuthenticationTokenFilter — JWT 登录受权过滤器
| ├── RestAuthenticationEntryPoint
| └── RestfulAccessDeniedHandler
├── config
| ├── IgnoreUrlsConfig
| └── SecurityConfig
└── util

 └── JwtTokenUtil -- JWT 的 token 解决工具类

复制代码
JwtAuthenticationTokenFilter 和 JwtTokenUtil 在讲 JWT 的时候曾经具体地讲过了,这里再简略补充一点。
客户端的申请头里携带了 token,服务端必定是须要针对每次申请解析校验 token 的,所以必须得定义一个过滤器,也就是 JwtAuthenticationTokenFilter:

从申请头中获取 token
对 token 进行解析、验签、校验过期工夫
校验胜利,将验证后果放到 ThreadLocal 中,供下次申请应用

重点来看其余四个类。第一个 RestAuthenticationEntryPoint(自定义返回后果:未登录或登录过期):
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {response.setHeader("Access-Control-Allow-Origin", "*");
    response.setHeader("Cache-Control","no-cache");
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json");
    response.getWriter().println(JSONUtil.parse(ResultObject.unauthorized(authException.getMessage())));
    response.getWriter().flush();
}

}
复制代码
能够通过 debug 的形式看一下返回的信息正是之前用户未登录状态下拜访文章页的错误信息。

具体的信息是在 ResultCode 类中定义的。
public enum ResultCode implements IErrorCode {

SUCCESS(0, "操作胜利"),
FAILED(500, "操作失败"),
VALIDATE_FAILED(506, "参数检验失败"),
UNAUTHORIZED(401, "暂未登录或 token 曾经过期"),
FORBIDDEN(403, "没有相干权限");
private long code;
private String message;

private ResultCode(long code, String message) {
    this.code = code;
    this.message = message;
}

}
复制代码
第二个 RestfulAccessDeniedHandler(自定义返回后果:没有权限拜访时):
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{

@Override
public void handle(HttpServletRequest request,
                   HttpServletResponse response,
                   AccessDeniedException e) throws IOException, ServletException {response.setHeader("Access-Control-Allow-Origin", "*");
    response.setHeader("Cache-Control","no-cache");
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json");
    response.getWriter().println(JSONUtil.parse(ResultObject.forbidden(e.getMessage())));
    response.getWriter().flush();
}

}
复制代码
第三个 IgnoreUrlsConfig(用于配置不须要平安爱护的资源门路):
@Getter
@Setter
@ConfigurationProperties(prefix = “secure.ignored”)
public class IgnoreUrlsConfig {

private List<String> urls = new ArrayList<>();

}
复制代码
通过 lombok 注解的形式间接将配置文件中不须要权限校验的门路放开,比如说 Knife4j 的接口文档页面。如果不放开的话,就被 SpringSecurity 拦挡了,没方法拜访到了。

第四个 SecurityConfig(SpringSecurity 通用配置):
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired(required = false)
private DynamicSecurityService dynamicSecurityService;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
    ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
            .authorizeRequests();

    // 不须要爱护的资源门路容许拜访
    for (String url : ignoreUrlsConfig().getUrls()) {registry.antMatchers(url).permitAll();}

    // 任何申请须要身份认证
    registry.and()
            .authorizeRequests()
            .anyRequest()
            .authenticated()
            // 敞开跨站申请防护及不应用 session
            .and()
            .csrf()
            .disable()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            // 自定义权限回绝解决类
            .and()
            .exceptionHandling()
            .accessDeniedHandler(restfulAccessDeniedHandler())
            .authenticationEntryPoint(restAuthenticationEntryPoint())
            // 自定义权限拦截器 JWT 过滤器
            .and()
            .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    // 有动静权限配置时增加动静权限校验过滤器
    if(dynamicSecurityService!=null){registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
    }
}

}
复制代码
这个类的次要作用就是通知 SpringSecurity 那些门路不须要拦挡,除此之外的,都要进行 RestfulAccessDeniedHandler(登录校验)、RestAuthenticationEntryPoint(权限校验)和 JwtAuthenticationTokenFilter(JWT 过滤)。
并且将 JwtAuthenticationTokenFilter 过滤器增加到 UsernamePasswordAuthenticationFilter 过滤器之前。
五、测试
第一步,测试登录接口,Apipost 间接拜访 http://localhost:9002/users/l…,能够看到 token 失常返回。

第二步,不带 token 间接拜访文章接口,能够看到进入了 RestAuthenticationEntryPoint 这个处理器:

第三步,携带 token,这次咱们改用 Knife4j 来测试,发现能够失常拜访:

源码链接:

github.com/itwanger/co…

本篇已收录至 GitHub 上星标 1.9k+ star 的开源专栏《Java 程序员进阶之路》,据说每一个优良的 Java 程序员都喜爱她,有趣风趣、通俗易懂。内容包含 Java 根底、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等外围知识点。学 Java,就认准 Java 程序员进阶之路😄。
github.com/itwanger/to…
star 了这个仓库就等于你领有了成为了一名优良 Java 工程师的后劲。也能够戳上面的链接跳转到《Java 程序员进阶之路》的官网网址,开始欢快的学习之旅吧。
tobebetterjavaer.com/

没有什么使我停留——除了目标,纵然岸旁有玫瑰、有绿荫、有平静的港湾,我是不系之舟。

正文完
 0