之前研究时候用mock.js和模拟登录接口完成Vue-Element-Admin的登录,生产很多场景会使用SpringSecurity框架,因为它功能较多,文档写的不错,所以现在使用真实的场景实现集成,security使用系列的过滤链完成请求的过滤和自定义,直接处理的是HttpServletResponse和HttpServletRequest对象。
EnableWebSecurity组合注解,包括 @Configuration,@EnableGlobalAuthentication, @Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class,OAuth2ImportSelector.class })说明是个配置类,并且要创建哪些bean,WebSecurityConfigurerAdapter适配器模式表明它默认有些配置,你只需要重写自己那部分配置即可,这里使用以下几个核心对象:
LoginAuthProvider:鉴权
JsonWebTokenUtil :支持登录的post
IRefreshTokenService:token刷新
RefreshTokenMapper:token的数据库操作
UserServiceImpl:用户校验
JsonWebTokenProperty:jwt自定义
使用方式如下:
-
- @EnableWebSecurity
- public class MasSecurity extends WebSecurityConfigurerAdapter {
- private static final Logger logger = LoggerFactory.getLogger(MasSecurity.class);
-
- @Autowired
- LoginAuthProvider loginAuthProvider;
-
- @Autowired
- JsonWebTokenUtil jsonWebTokenUtil;
- @Autowired
- IRefreshTokenService refreshTokenService;
-
- @Autowired
- RefreshTokenMapper refreshTokenMapper;
-
- @Autowired
- UserServiceImpl userService;
-
- @Autowired
- JsonWebTokenProperty jsonWebTokenProperty;
-
- // 先认证后鉴权
- @Override
- protected void configure(AuthenticationManagerBuilder auth) throws Exception {
- auth.authenticationProvider(loginAuthProvider);
- }
-
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- //允许跨域,配置后SpringSecurity会自动寻找name=corsConfigurationSource的Bean
- http.cors();
- http.csrf().disable();
- //当访问接口失败的配置
- http.exceptionHandling().authenticationEntryPoint(new InterfaceAccessException());
- http.authorizeRequests()
- .antMatchers("/","/login", "/swagger-ui.html","/swagger-resources/**","/webjars/**","/v2/**","/api/**").permitAll()
- .anyRequest().authenticated()
- .and()
- .formLogin().loginProcessingUrl("/login")
- .and().addFilterAt(jsonAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
- .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//因为用不到session,所以选择禁用
- http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler()).deleteCookies(jsonWebTokenProperty.getHeader()).clearAuthentication(true);
- http.addFilterAfter(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
-
- }
-
- @Bean(name="myRequestMatcher")
- public SkipPathAntMatcher skipPathAntMatcher(){
- List
uris = new LinkedList<>(); - uris.add("/login");
- uris.add("/swagger-resources/**");
- uris.add("/webjars/**");
- uris.add("/v2/**");
- uris.add("/api/**");
- uris.add("/swagger-ui.html");
- return new SkipPathAntMatcher(uris);
- }
-
- @Bean
- public JwtAuthenticationFilter jwtAuthenticationFilter() {
- return new JwtAuthenticationFilter(jsonWebTokenUtil, userService,skipPathAntMatcher(), refreshTokenMapper);
- }
-
-
- @Bean
- public JsonAuthenticationFilter jsonAuthenticationFilter() throws Exception {
- JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
- filter.setAuthenticationManager(authenticationManagerBean());
- filter.setFilterProcessesUrl("/login");
- filter.setAuthenticationSuccessHandler(new MySuccessHandler());
- filter.setAuthenticationFailureHandler(new MyFailHandler());
- return filter;
- }
-
-
- @Bean
- public LogoutSuccessHandler logoutSuccessHandler() {
- return new LogoutSuccessHandler() {
- @Autowired
- public void setObjectMapper(ObjectMapper objectMapper) {
- this.objectMapper = objectMapper;
- }
-
- private ObjectMapper objectMapper;
-
- @Override
- public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
- response.setContentType("application/json;charset=utf-8");
- PrintWriter out = response.getWriter();
- out.write(objectMapper.writeValueAsString(ResponseData.success("logout success.")));
- out.flush();
- out.close();
- }
-
- /* // 这种写法更简洁
- @Override
- public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
- response.setContentType("application/json;charset=utf-8");
- PrintWriter out = response.getWriter();
- out.write(new ObjectMapper().writeValueAsString(ResponseData.success("logout success.")));
- out.flush();
- out.close();*/
- };
- }
-
- //登录成功的处理类
- class MySuccessHandler implements AuthenticationSuccessHandler {
- @Override
- public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
- Authentication authentication) throws IOException, ServletException {
- UserDetails details = (UserDetails) authentication.getPrincipal();
- List
roles = (List) details.getAuthorities(); - //登录时同时生成refreshToken,保存到表中
- logger.info("begin to onAuthenticationSuccess.");
- RefreshTokenEntity token = new RefreshTokenEntity();
- token.setUsename(details.getUsername());
- String refreshToken = jsonWebTokenUtil.generateRefreshToken(details, roles.get(0).getAuthority());
- token.setToken(refreshToken);
- LambdaQueryWrapper
queryWrapper = new QueryWrapper().lambda().eq(RefreshTokenEntity::getUsename, details.getUsername()); - RefreshTokenEntity refreshTokenTemp = refreshTokenMapper.selectOne(queryWrapper);
- if (refreshTokenTemp != null) {
- refreshTokenTemp.setToken(refreshToken);
- refreshTokenMapper.update(refreshTokenTemp, queryWrapper);
- } else {
- logger.info("begin to insert token");
- refreshTokenMapper.insert(token);
- logger.info("end to insert token");
- }
- response.setHeader(jsonWebTokenUtil.getHeader(), refreshToken);
- /* Cookie cookie = new Cookie("token", refreshToken);
- cookie.setPath("/");
- response.addCookie(cookie)
- JSONObject jsonObject = new JSONObject();
- jsonObject.put("status", true);
- jsonObject.put("code",20000);
- jsonObject.put("data",refreshToken);
- out.write(new ObjectMapper().writeValueAsString(jsonObject));
- ;*/
- PrintWriter out = response.getWriter();
- out.write(new ObjectMapper().writeValueAsString(ResponseData.success(refreshToken)));
- out.flush();
- out.close();
- ResponseData.success(response);
-
- }
- }
-
- //登录失败的处理
- public class MyFailHandler implements AuthenticationFailureHandler {
-
- @Override
- public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
- AuthenticationException e) throws IOException, ServletException {
- ResponseMsgUtil.sendFailMsg(e.getMessage(), response);
- }
- }
-
-
- }
这里介绍比较重要的一个jwt过滤逻辑,所有的请求都必须携带token,并且跟库里的token比较,未携带或不可用就返回无token或超时,对应vue-element-admin里面的50017的code:
-
- public class JwtAuthenticationFilter extends OncePerRequestFilter {
- private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
-
- private JsonWebTokenUtil tokenUtil;
- private UserServiceImpl userServiceImpl;
- private RequestMatcher requestMatcher;
- //private List
requestMatcher; - private RefreshTokenMapper refreshTokenMapper;
-
- public JwtAuthenticationFilter(JsonWebTokenUtil tokenUtil, UserServiceImpl userServiceImpl, RequestMatcher requestMatcher,RefreshTokenMapper refreshTokenMapper) {
- this.tokenUtil = tokenUtil;
- this.userServiceImpl = userServiceImpl;
- this.requestMatcher = requestMatcher;
- this.refreshTokenMapper = refreshTokenMapper;
- }
-
- @Override
- protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
- logger.info(String.format("请求路径-->%s,是否应该被过滤掉-->%s",request.getRequestURL(),requestMatcher.matches(request)));
- return requestMatcher.matches(request);
- }
-
- @Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
- throws ServletException, IOException {
- //从请求头部获取json web token
- String jwt = request.getHeader(tokenUtil.getHeader());
- logger.info("urls:"+request.getRequestURI()+"XXXXXX"+request.getRequestURI().substring(0,request.getRequestURL().indexOf("/")));
- if (StringUtils.hasLength(jwt) && !jwt.equals("null") && !jwt.equals("undefined")) {
- //从jwt中获取用户名,这里应该考虑过期时间,超过过期时间的话获取不到username
- //TODO
- String username = tokenUtil.getUsernameFromToken(jwt);
- if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
- LambdaQueryWrapper
queryWrapper = new QueryWrapper().lambda().eq(RefreshTokenEntity::getUsename, username); - RefreshTokenEntity refreshTokenEntity = refreshTokenMapper.selectOne(queryWrapper);
- String tokenInMysql = StringUtils.isEmpty(refreshTokenEntity) ? "null" : refreshTokenEntity.getToken();
- if (jwt.equals(tokenInMysql)) {
- logger.info("JwtAuthenticationFilter,username:" + username);
- //通过用户名查询
- UserDetails userDetails = userServiceImpl.loadUserByUsername(username);
- //创建认证信息
- UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username,
- userDetails.getPassword(), userDetails.getAuthorities());
- SecurityContextHolder.getContext().setAuthentication(authentication);
- }
- }
- filterChain.doFilter(request, response);
- } else {
- //logger.info(requestMatcher.toString());
- reject(request, response);
- }
-
- }
-
- private static void reject(HttpServletRequest request,HttpServletResponse response) throws IOException{
- /* JSONObject jsonObject = new JSONObject();
- jsonObject.put("status", true);
- jsonObject.put("code",50054);
- jsonObject.put("data","Token expired, please log in again.");
- PrintWriter out = response.getWriter();
- out.write(new ObjectMapper().writeValueAsString(jsonObject));*/
- logger.info("filtered url:"+request.getRequestURI());
- PrintWriter out = response.getWriter();
- out.write(new ObjectMapper().writeValueAsString(ResponseData.fail(ApiError.from(ApiErrorEnum.HAVE_NO_TOKEN))));
- out.flush();
- }
-
- }
因login方法在security中做拦截和返回,但vue-element-admin的每次路由都要进行权限的校验,所以需要构造该/getInfo方法,每次返回的是user对应的role;
- public ResponseData loginInfo(HttpServletRequest request) {
- // 因为所有url都被过滤,所以这里只需要拿到role就行,不需要考虑token是否过期问题
- String token = request.getHeader(jwtTokenUtil.getHeader());
- LoginInfoVO loginInfoVO = new LoginInfoVO();
- if(StringUtils.hasLength(token) && !token.equals("null")) {
- String username = jwtTokenUtil.getUsernameIgnoreExpiration(token);
- loginInfoVO.setName(username);
- LambdaQueryWrapper
queryWrapper = new QueryWrapper().lambda().eq(RoleEntity::getName, username); - List
roleEntities = roleDao.selectList(queryWrapper); - List
roleList = new ArrayList<>(); - for (RoleEntity roleEntity : roleEntities) {
- roleList.add(roleEntity.getName());
- }
- loginInfoVO.setRoles(roleList);
- return ResponseData.success(loginInfoVO);
- }
- return ResponseData.fail(ApiError.from(ApiErrorEnum.TOKEN_EXPIRED));
- }
换了一个登录页,如下,登录后返回token,key已修改成JWTHeaderName:
