网络安全的重要性不言而喻,如今早已不是以前随便弄个http请求就能爬到数据的时代,而作为一个架构师,网络安全必须在产品开发之初就考虑好。因为在产品开发的后期,一方面是客户增多,压力变大,可供利用的时间也会变少,另一方面,随着时间的推移,项目越发庞大,这个时候想要在开发继续推进的同时来调整架构,带来的影响不可谓不大。

同时,对于多用户系统来说,必然涉及到用户角色管理,不同的用户不可能具有相同的权限,而如果要通过硬编码来实现,那工作量可就大了,而且配置起来也极其不灵活,二通过架构方式来编写这些框架代码,那也是不小的工程量,而且对架构师水平要求也不低。

所幸Springboot就自带了安全模块,我们可以在前期设计的时候很轻松的引入这些模块。如果这些模块让我们自己来写一遍,自然是很痛苦的,更主要的是,你写完了不一定有他的好,Web开发最好的地方在于他有太多的脚手架可直接使用。在这里我们需要用到的就是SpringSecurity及JWT。

JWT

JWT是JSON WEB TOKEN的缩写,它是基于 RFC 7519 标准定义的一种可以安全传输的的JSON对象,由于使用了数字签名,所以是可信任和安全的。

JWT的组成

我们先来看一个JWT加密后的token:

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE1NTY3NzkxMjUzMDksImV4cCI6MTU1NzM4MzkyNX0.d-iki0193X0bBOETf2UN3r3PotNIEAV7mzIxxeI5IxFyzzkOZxS0PGfF_SK6wxCv2K8S0cZjMkv6b5bCqc0VBw

只是看这个字符串会觉得一脸懵逼,它实际上是遗传加密过的字符串,我们可以在jwt.io/解析出它的原文:

springboot 开发接口鉴权_spring boot

在右边的解析中给出了他内容解析说明,它是由三部分构成:

JWT token的格式:header.payload.signature

  • header中用于存放签名的生成算法
{"alg": "HS512"}
  • payload中用于存放用户名、token的生成时间和过期时间
{"sub":"admin","created":1489079981393,"exp":1489684781}
  • signature为以header和payload生成的签名,一旦header和payload被篡改,验证将失败
//secret为加密算法的密钥
String signature = HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)

既然jwt是可以被解析的,那么它到底解决了什么问题呢?我们一般设计授权模块,就是做一个加密的token,然后让客户端在每次请求的时候都带上这个token,我们会对token解析并校验是否合法,不合法就会拒绝服务,jwt健康相当于把这部分功能给我们做了:

  • 用户调用登录接口,登录成功后获取到JWT的token;
  • 之后用户每次调用接口都在http的header中添加一个叫Authorization的头,值为JWT的token;
  • 后台程序通过对Authorization头中信息的解码及数字签名校验来获取其中的用户信息,从而实现认证和授权。

环境

引入安全模块主要用到两个模块,一个是springboot自带的security模块,另一个是jwt模块。在实际项目实战用,为了不重复造轮子,还需要应用hutool这个三方依赖。这个依赖带了一些工具类方法,有点像Android的Xutil可以为我们开发节省很多时间,在这里主要是用来做加解密。如果你有其他更合适的只要能达到效果就行。

依赖

在pom.xml中添加项目依赖

<!--SpringSecurity依赖配置-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--Hutool Java工具包-->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>4.5.7</version>
</dependency>
<!--JWT(Json Web Token)登录支持-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

然后我们需要一些功能性的工具,根据我们的业务我们需要三个功能:

  • 根据用户信息生成对应的token,generateToken(UserDetails userDetails)
  • 用户用token请求后我们还需要解析这个token,因此需要一个解析的方法getUserNameFromToken(String token)
  • 解析token后需要判断是否有效,这里主要是做通用有效性判断:validateToken(String token, UserDetails userDetails)

好了,我们把这个实现下:

package org.lange.mall.common.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JwtToken生成的工具类
 * JWT token的格式:header.payload.signature
 * header的格式(算法、token的类型):
 * {"alg": "HS512","typ": "JWT"}
 * payload的格式(用户名、创建时间、生成时间):
 * {"sub":"wang","created":1489079981393,"exp":1489684781}
 * signature的生成算法:
 * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
 */
@Component
public class JwtTokenUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;

    /**
     * 根据负载生成JWT的token
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 从token中获取JWT中的负载
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.info("JWT格式验证失败:{}", token);
        }
        return claims;
    }

    /**
     * 生成token的过期时间
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 从token中获取登录用户名
     */
    public String getUserNameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 验证token是否还有效
     *
     * @param token       客户端传入的token
     * @param userDetails 从数据库中查询出来的用户信息
     */
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     * 判断token是否已经失效
     */
    private boolean isTokenExpired(String token) {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

    /**
     * 从token中获取过期时间
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     * 根据用户信息生成token
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    /**
     * 判断token是否可以被刷新
     */
    public boolean canRefresh(String token) {
        return !isTokenExpired(token);
    }

    /**
     * 刷新token
     */
    public String refreshToken(String token) {
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }
}

上面用到了@Component注解,这个注解和@Service,@Controller使用类似,都是在需要使用的类中通过@Autowired连接就能自动使用。这其实是SpringBoot的Bean管理机制,在组件的使用中,根本不需要去new对象。

配置SpringSecurity

和前面我们了解的Springboot的Mybatis,Swagger一样,使用SpringSecurity需要添加配置类自动化配置。这里需要用到三个额外的注解:

@Configuration
@Autowired
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Bean

前面两个我们前面用过了。EnableWebSecurity作为Springboot自带的一个注解,他的作用就是让项目运行的时候启动安全模块。而@EnableGlobalMethodSecurity,同时这个注解为我们提供了prePostEnabled 、securedEnabled 和 jsr250Enabled 三种不同的机制来实现同一种功能。

@Autowired不仅仅可以用在对象实例化上给成员变量的使用提供getset方法,还可以注解在方法上,而@Bean如果注解在一个返回对象的方法上,调用的时候可以直接使用该对象而不用调用方法来获取对象。

对于SpringSecurity的配置我们需要创建一个配置类SecurityConfig.java

package org.lange.mall.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


/**
 * SpringSecurity的配置
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
    @Autowired
    private IgnoreUrlsConfig ignoreUrlsConfig;

    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
                .authorizeRequests();
        //不需要保护的资源路径允许访问
        for (String url : ignoreUrlsConfig.getUrls()) {
            registry.antMatchers(url).permitAll();
        }
        //允许跨域请求的OPTIONS请求
        registry.antMatchers(HttpMethod.OPTIONS)
                .permitAll();
        httpSecurity.csrf()// 由于使用的是JWT,我们这里不需要csrf
                .disable()
                .sessionManagement()// 基于token,所以不需要session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .anyRequest()// 除上面外的所有请求全部需要鉴权认证
                .authenticated();
        // 禁用缓存
        httpSecurity.headers().cacheControl();
        // 添加JWT filter
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        //添加自定义未授权和未登录结果返回
        httpSecurity.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);
        return httpSecurity.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilter();
    }

}

在上面的配置类里面,最重要的两个方法:filterChain和passwordEncoder,这两个是提供给系统调用的。一个是用来创建拦截器配置,这个拦截器决定了请求放行规则,这个怎么理解呢?比如某些分享链接,或者用户还没有登陆的时候,那他是什么权限也没有的。如果不给放行,那连入口都没有,谈何使用。这就是放行的作用,同时,一些外部的Api调用,也需要在这里配置。

把这个说明白那一串代码就很好理解了。我们要放行某些资源路径,要配置一些跨域请求,然后这里还禁用了缓存,添加了一个token过滤器,用来过滤不正常的请求。然后就是配置了授权不通过的返回,大概就是这些了。这里资源路劲比较多,所以单独放在类IgnoreUrlsConfig中,为了演示方便,这个类中没啥东西:

package org.lange.mall.config.security;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {

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

然后我们来看下授权过滤器要怎么写,JwtAuthenticationTokenFilter.java

package org.lange.mall.component;

import org.lange.mall.common.util.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {//继承了每次请求一次的过滤器,还有其他的过滤器可以使用
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        // 拿到请求头,因为验证信息都放在通用请求头里面的
        String authHeader = request.getHeader(this.tokenHeader);
        // 判断是否有token
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            //提取出来token
            String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
            // token中包含username,我们提取出来
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            LOGGER.info("checking username:{}", username);
            // 能提取到username 并且开了安全验证就继续
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                // 通过这个username在service里面查询用户详细
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                // 判断token数据和用户详情是否对的上切token没有过期
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    //校验通过更新用户详细
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    LOGGER.info("authenticated user:{}", username);
                    //告诉安全机制验证通过
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        // 执行过滤逻辑
        chain.doFilter(request, response);
    }
}

如果没有通过验证,那我们要禁止他访问,返回通用禁止响应,这个很好理解。

package org.lange.mall.component;

import cn.hutool.json.JSONUtil;
import org.lange.mall.common.api.CommonResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 当访问接口没有权限时,自定义的返回结果
 * 这里是通用禁止响应
 */
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
        response.getWriter().flush();
    }
}

如果用户没有登陆就请求了,我们不应该返回禁止,而是返回未授权的提示,这个编码和禁止差不多,只不过需要实现的接口不一样:

package org.lange.mall.component;

import cn.hutool.json.JSONUtil;
import org.lange.mall.common.api.CommonResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 当未登录或者token失效访问接口时,自定义的返回结果
 */
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage())));
        response.getWriter().flush();
    }
}

到这里已经完成及安全模块的配置,不过我们前面写的配置好像和用户角色管理没有关系,因为我们没有实现我们自己的用户管理服务呀,那我们要来实现以下MallSecurityConfig.java:

package org.lange.mall.config.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

/**
 * 自定义配置,用于配置如何获取用户信息
 */
@Configuration
public class MallSecurityConfig {

    @Autowired
    private UmsAdminService adminService;

    @Bean
    public UserDetailsService userDetailsService() {
        //获取登录用户信息
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                AdminUserDetails admin = adminService.getAdminByUsername(username);
                if (admin != null) {
                    return admin;
                }
                throw new UsernameNotFoundException("用户名或密码错误");
            }
        };
    }
}

顺便实现对应的bean和service,这个bean要实现Springboot的UserDetails,不然会找不到。我们后续是可以扩展这个用户信息类的,这里为了掩饰就不扩展了。

package org.lange.mall.data;

import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * SpringSecurity用户信息封装类
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Builder
public class AdminUserDetails implements UserDetails {
    private String username;
    private String password;
    private List<String> authorityList;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorityList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    }

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

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

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

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

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

    @Override
    public boolean isEnabled() {
        return true;
    }
}

然后就是对应的service,面向接口编程是个好习惯,有利于规范化。UmsAdminService.java:

package org.lange.mall.service.impl;


import cn.hutool.core.collection.CollUtil;
import lombok.extern.slf4j.Slf4j;
import org.lange.mall.common.util.JwtTokenUtil;
import org.lange.mall.data.AdminUserDetails;
import org.lange.mall.service.UmsAdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
    /**
     * 存放默认用户信息
     */
    private List<AdminUserDetails> adminUserDetailsList = new ArrayList<>();
    // 存放默认资源信息
    //private List<UmsResource> resourceList = new ArrayList<>();
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @PostConstruct
    private void init(){
        adminUserDetailsList.add(AdminUserDetails.builder()
                .username("admin")
                .password(passwordEncoder.encode("123456"))
                .authorityList(CollUtil.toList("brand:create","brand:update","brand:delete","brand:list","brand:listAll"))
                .build());
        adminUserDetailsList.add(AdminUserDetails.builder()
                .username("macro")
                .password(passwordEncoder.encode("123456"))
                .authorityList(CollUtil.toList("brand:listAll"))
                .build());
        /*resourceList.add(UmsResource.builder()
                .id(1L)
                .name("brand:create")
                .url("/brand/create")
                .build());
        resourceList.add(UmsResource.builder()
                .id(2L)
                .name("brand:update")
                .url("/brand/update/**")
                .build());
        resourceList.add(UmsResource.builder()
                .id(3L)
                .name("brand:delete")
                .url("/brand/delete/**")
                .build());
        resourceList.add(UmsResource.builder()
                .id(4L)
                .name("brand:list")
                .url("/brand/list")
                .build());
        resourceList.add(UmsResource.builder()
                .id(5L)
                .name("brand:listAll")
                .url("/brand/listAll")
                .build());*/
    }
    @Override
    public AdminUserDetails getAdminByUsername(String username) {
        List<AdminUserDetails> findList = adminUserDetailsList.stream().filter(item -> item.getUsername().equals(username)).collect(Collectors.toList());
        if(CollUtil.isNotEmpty(findList)){
            return findList.get(0);
        }
        return null;
    }

//    @Override
//    public List<UmsResource> getResourceList() {
//        return resourceList;
//    }

    @Override
    public String login(String username, String password) {
        String token = null;
        try {
            UserDetails userDetails = getAdminByUsername(username);
            if(userDetails==null){
                return token;
            }
            if (!passwordEncoder.matches(password, userDetails.getPassword())) {
                throw new BadCredentialsException("密码不正确");
            }
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
            token = jwtTokenUtil.generateToken(userDetails);
        } catch (AuthenticationException e) {
            log.warn("登录异常:{}", e.getMessage());
        }
        return token;
    }
}

到这里业务相关的鉴权我们写完了,但是我们还没有接入到接口中,也就是用户要从哪里请求我们要定义好。我们来实现以下登录接口,通常的登录逻辑是用户拿用户名和密码来登录,登录成功我们返回token,这个时候用户就能用这个token去请求他想要的资源了。如果用户没有登陆就去请求,就会提示未登录,现在我们来实现下。

先定义接口控制器UmsAdminController.java,就两个方法,一个登录,一个请求资源列表。在此之前我们把资源的这个类定义一下:

package org.lange.mall.data;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.util.Date;

/**
 * 后台资源表
 */
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="UmsResource对象", description="后台资源表")
@Builder
public class UmsResource{

    private Long id;

    @ApiModelProperty(value = "创建时间")
    private Date createTime;

    @ApiModelProperty(value = "资源名称")
    private String name;

    @ApiModelProperty(value = "资源URL")
    private String url;

    @ApiModelProperty(value = "描述")
    private String description;

    @ApiModelProperty(value = "资源分类ID")
    private Long categoryId;

}

然后要把service中获取资源列表的方法放开一下,前面被我注释了,UmsAdminController.java

package org.lange.mall.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.lange.mall.common.api.CommonResult;
import org.lange.mall.data.UmsResource;
import org.lange.mall.service.UmsAdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
@Api(tags = "UmsAdminController")
@Tag(name = "UmsAdminController", description = "后台用户管理")
@RequestMapping("/admin")
public class UmsAdminController {
    @Autowired
    private UmsAdminService adminService;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;


    @ApiOperation(value = "登录以后返回token")
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    @ResponseBody
    public CommonResult login(@RequestParam String username, @RequestParam String password) {
        String token = adminService.login(username, password);
        if (token == null) {
            return CommonResult.validateFailed("用户名或密码错误");
        }
        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("token", token);
        tokenMap.put("tokenHead", tokenHead);
        return CommonResult.success(tokenMap);
    }

    @ApiOperation(value = "请求资源列表")
    @RequestMapping(value = "/resourceList", method = RequestMethod.POST)
    @ResponseBody
    public CommonResult<List<UmsResource>> resourceList() {
        List<UmsResource> resourceList = adminService.getResourceList();
        return CommonResult.success(resourceList);
    }
}

然后就是运行,测试看效果。