Web菜鸟教程 - Springboot接入认证授权模块

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

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

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

JWT

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

JWT的组成

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

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE1NTY3NzkxMjUzMDksImV4cCI6MTU1NzM4MzkyNX0.d-iki0193X0bBOETf2UN3r3PotNIEAV7mzIxxeI5IxFyzzkOZxS0PGfF_SK6wxCv2K8S0cZjMkv6b5bCqc0VBw

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

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

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);
    }
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/78400.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JupyterHub实战应用

一、JupyerHub jupyter notebook 是一个非常有用的工具&#xff0c;我们可以在浏览器中任意编辑调试我们的python代码&#xff0c;并且支持markdown 语法&#xff0c;可以说是科研利器。但是这种情况适合个人使用&#xff0c;也就是jupyter notebook以我们自己的主机作为服务器…

iOS设计规范是什么?都有哪些具体规范

iOS设计规范是苹果为移动设备操作系统iOS制定的设计指南。iOS设计规范的制定保证了苹果应用在外观和操作上的一致性和可用性&#xff0c;从而提高了苹果界面设计的用户体验和应用程序的成功性。本文将从七个方面全面分析iOS设计规范。 1.iOS设计规范完整版分享 由「即时设计」…

容器和云原生(二):Docker容器化技术

目录 Docker容器的使用 Docker容器关键技术 Namespace Cgroups UnionFS Docker容器的使用 首先直观地了解docker如何安装使用&#xff0c;并快速启动mysql服务的&#xff0c;启动时候绑定主机上的3306端口&#xff0c;查找mysql容器的ip&#xff0c;使用mysql -h contain…

设计模式——建造者(Builder)模式

建造者模式&#xff08;Builder Pattern&#xff09;&#xff0c;又叫生成器模式&#xff0c;是一种对象构建模式 它可以将复杂对象的建造过程抽象出来&#xff0c;使这个抽象过程的不同实现方法可以构造出不同表现的对象。建造者模式是一步一步创建一个复杂的对象&#xff0c;…

Python文件操作教程,Python文件操作笔记

文件的打开与关闭 想一想&#xff1a; 如果想用word编写一份简历&#xff0c;应该有哪些流程呢&#xff1f; 打开word软件&#xff0c;新建一个word文件写入个人简历信息保存文件关闭word软件 同样&#xff0c;在操作文件的整体过程与使用word编写一份简历的过程是很相似的…

使用 BERT 进行文本分类 (01/3)

摄影&#xff1a;Max Chen on Unsplash 一、说明 这是使用 BERT 语言模型的一系列文本分类演示的第一部分。以文本的分类作为例&#xff0c;演示它们的调用过程。 二、什么是伯特&#xff1f; BERT 代表 来自变压器的双向编码器表示。 首先&#xff0c;转换器是一种深度学习模…

【Linux命令详解 | gzip命令】 gzip命令用于压缩文件,可以显著减小文件大小

文章标题 简介一&#xff0c;参数列表二&#xff0c;使用介绍1. 基本压缩和解压2. 压缩目录3. 查看压缩文件内容4. 测试压缩文件的完整性5. 强制压缩6. 压缩级别7. 与其他命令结合使用8. 压缩多个文件9. 自动删除原文件 总结 简介 在Linux中&#xff0c;gzip命令是一款强大的文…

C语言笔试训练【第九天】

文章目录 &#x1f47f;1、下列程序的输出是&#xff08; &#xff09;&#x1f48e;2、二维数组X按行顺序存储&#xff0c;其中每个元素占1个存储单元。若 X[4][4] 的存储地址为 Oxf8b82140 , X[9][9] 的存储地址为 Oxf8b8221c ,则 X[7][7] 的存储地址为&#xff08; &#xf…

交换实验一

题目 交换机上接口配置 SW1 interface GigabitEthernet0/0/1 port hybrid tagged vlan 2 port hybrid untagged vlan 3 to 6 interface Ethernet0/0/2 port hybrid pvid vlan 3 port hybrid untagged vlan 2 to 6 interface Ethernet0/0/3 port link-type access port d…

C++基础语法——继承

1.继承是什么&#xff1f; 继承是一种面向对象编程的概念&#xff0c;它允许一个类&#xff08;称为子类或派生类&#xff09;从另一个类&#xff08;称为基类或父类&#xff09;继承属性和方法。继承使得子类能够使用基类已有的代码&#xff0c;并且可以在此基础上进行扩展或修…

99页4万字XX大数据湖项目建设方案

导读&#xff1a;原文《99页4万字XX大数据湖项目建设方案》&#xff08;获取来源见文尾&#xff09;&#xff0c;本文精选其中精华及架构部分&#xff0c;逻辑清晰、内容完整&#xff0c;为快速形成售前方案提供参考。 目 录 1.项目综述 1.1.项目背景 1.2.项目目标 1.3.项…

Error = [Microsoft][SQL Server Native Client 10.0]无法打开 BCP 主数据文件(已解决)

需求&#xff1a;分页方式查出表中数据存入excel中&#xff0c;并给excel标记不同的编号 前提&#xff1a; #all_4表中数据已准备好 版本&#xff1a; sql server 2008 declare n int set n 1 while n 100 begin print n:print nEXEC master.dbo.sp_configure show adva…

什么是flexbox布局?它有什么特点和优势?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 什么是 Flexbox 布局&#xff1f;⭐ 特点和优势⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个专栏是为那…

(三)行为模式:1、责任链模式(Chain of Responsibility Pattern)(C++示例)

目录 1、责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;含义 2、责任链模式的UML图学习 3、责任链模式的应用场景 4、责任链模式的优缺点 5、C实现责任链模式的实例 1、责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;含义 责任…

2682. 找出转圈游戏输家

题目描述&#xff1a; n 个朋友在玩游戏。这些朋友坐成一个圈&#xff0c;按 顺时针方向 从 1 到 n 编号。从第 i 个朋友的位置开始顺时针移动 1 步会到达第 (i 1) 个朋友的位置&#xff08;1 < i < n&#xff09;&#xff0c;而从第 n 个朋友的位置开始顺时针移动 1 步…

vue之动态表单(优化)

代码资源在这儿 ↑ vue之动态表单优化 vue2js动态表单优化vue3ts动态表单优化 vue2js动态表单优化 效果图 目录结构 五个文件的完整代码: 以下是App.vue <template><div><router-view></router-view><Formpage /></div> </templa…

jenkins 安装nodejs 14

参考&#xff1a; jenkins容器安装nodejs-前端问答-PHP中文网

在ubuntu+cpolar+rabbitMQ环境下,实现mq服务端远程访问

文章目录 前言1.安装erlang 语言2.安装rabbitMQ3. 内网穿透3.1 安装cpolar内网穿透(支持一键自动安装脚本)3.2 创建HTTP隧道 4. 公网远程连接5.固定公网TCP地址5.1 保留一个固定的公网TCP端口地址5.2 配置固定公网TCP端口地址 前言 RabbitMQ是一个在 AMQP(高级消息队列协议)基…

LangChain手记 Agent 智能体

整理并翻译自DeepLearning.AILangChain的官方课程&#xff1a;Agent&#xff08;源代码可见&#xff09; “人们有时会将LLM看作是知识库&#xff0c;因为它被训练所以记住了来自互联网或其他地方的海量信息&#xff0c;因而当你向它提问时&#xff0c;它可以回答你的问题。有一…

网络

mcq Java 传输层&#xff1a;拆分和组装&#xff0c;完成端到端的消息传递&#xff0c;流量控制&#xff0c;差错控制等 网络层&#xff1a; 寻址、路由&#xff0c;复用&#xff0c;拥塞控制&#xff0c;完成源到宿的传递。 显然A选项是错误的&#xff0c;有流量控制的是传输层…