电商项目实战(架构四)——SpringSecurity + JWT实现认证与授权进行用户登录

一、前言

  登录和授权模块是整个项目的安全锁,是开发中的必要一环。本文通过使用SpringSecurity安全框架和JWT实现后台的登录和授权,为了方便接口的在线测试,对swagger-ui的配置文件进行改造,使其能够拿到登录令牌token。

二、介绍

  1、SpringSecurity

  SpringSecurity是一个高性能的认证与授权的框架,为java项目提供了认证和授权的功能。

  2、JWT

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

  JWT的完全体是token,token的组成分为三部分:header,payload,signature;

  header存放的是生成签名的算法标准

{"alg":"HS512"}

  payload是验证主体,存放用户名,token的创建时间和过期时间

{"sub":"admin","created":"1574405893304","end":"1575010693"}

  signature是以header和payload为主体生成的签名验证,一旦header或payload被篡改,签名将验证失败。

String signature = HMACSHA512(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret);

  JWT实例

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE1NzQ0MDU4OTMzMDQsImV4cCI6MTU3NTAxMDY5M30.TOM6eciLWaeDPUV5Q1CfISzN0H1-4TYCriKD2FOkcznHpkoJPDID105xdG0D_vlGlx0FweGvoMphISga7VAUmw

  解析JWT,进入:https://jwt.io/

  

三、实现认证与授权的流程说明

  1、用户调用登录接口进行登录,成功后获取到生成的token;

  2、之后用户每次调用接口都会在http的header请求头中添加key为Authorization,值为获取到的token;

  3、后台过滤器会解析请求头中Authorization的值token,校验数字签名,获取封装的用户信息,实现认证与授权。

四、Hutool工具包

  Hutool工具包可以帮助开发者简化代码,优化代码。

五、mysql新建数据库表

  1、ums_admin:用户表

  

  2、ums_role:用户角色表

  

  3、ums_permission:用户权限表

  

  4、ums_admin_role_relation:用户角色关系表

  

  5、ums_role_permission_relation:角色权限关系表

  

  6、ums_admin_permission_relation:用户权限关系表

  

  7、mybatis逆向生成相应的model,example,mapper,mapper.xml

  resource包下修改generatorConfig.xml文件,添加生成表名

<!--生成全部表tableName设为%-->

        <!--商品品牌表-->
        <table tableName="pms_brand"></table>
        <!--用户表-->
        <table tableName="ums_admin"></table>
        <!--角色表-->
        <table tableName="ums_role"></table>
        <!--权限表-->
        <table tableName="ums_permission"></table>
        <!--用户角色关系表-->
        <table tableName="ums_admin_role_relation"></table>
        <!--角色权限关系表-->
        <table tableName="ums_role_permission_relation"></table>
        <!--用户权限关系表-->
        <table tableName="ums_admin_permission_relation"></table>

  运行mbg包下Generator程序,自动生成代码

  

六、项目整合SpringSecurity安全框架

  1、添加相关pom.xml依赖配置

<!--SpringSecurity依赖配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--hutool工具包依赖配置-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.5.7</version>
        </dependency>
        <!--JWT依赖配置-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

  2、在application.yml中添加自定义jwt配置

#项目启动端口
server:
  port: 10077
spring:
  #连接mysql数据库
  datasource:
    url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
  #连接redis缓存数据库
  redis:
    host: localhost    #redis服务器地址
    database: 0        #redis数据库索引(默认为0)
    port: 6379         #redis服务器连接端口
    password:          #redis连接密码(默认为空)
    #redis连接池
    jedis:
      pool:
        max-wait: -1ms  #连接池最大阻塞等待时间(使用负值表示没有限制)
        min-idle: 0     #连接池中的最小空闲连接
        max-idle: 8     #连接池中的最大空闲连接
        max-active: 8   #连接池最大连接数(使用负值表示没有限制)
    timeout: 3000ms     #连接超时时间(毫秒)

#redis自定义配置
redis:
  key:
    prefix:
      authCode: "portal:authCode:"
    expire:
      authCode: 120

#mybatis映射xml文件路径
mybatis:
  mapper-locations:
    classpath: com/zzb/test/admin/*/*.xml  

#jwt自定义配置
jwt:
  tokenHeader: Authorization  #JWT存储的请求头
  secret: mySecret            #JWT加解密使用的密钥
  expiration: 604800          #JWT的超期时间(60*60*24)
  tokenHead: Bearer           #JWT负载中拿到开头

  3、新建存放通用工具类的包utils,在utils包下新建JWT的通用工具类

  

   JwtTokenUtil类

package com.zzb.test.admin.utils;

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 org.springframework.util.StringUtils;

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

/**
 * JwtToken的生成工具类
 * Created by zzb on 2019/11/21 10:23
 */
@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;

    /**
     * 封装用户信息,并生成token
     * @param userDetails
     * @return
     */
    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 this.generateTokenByClaims(claims);
    }

    /**
     * 根据负载生成token
     * @param claims
     * @return
     */
    public String generateTokenByClaims(Map<String,Object> claims){
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(this.generationExpirationDate())
                .signWith(SignatureAlgorithm.HS512,secret)
                .compact();
    }

    /**
     * 解析token,获取负载主体
     * @param token
     * @return
     */
    public 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,获取负载主体中的用户名
     * @param token
     * @return
     */
    public String getUserNameFromToken(String token){
        String username = null;
        try {
            Claims claims = this.getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            logger.info("JWT解析token失败:{}",token);
        }

        return username;
    }

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

    /**
     * 验证token是否有效
     * @param token
     * @param userDetails
     * @return
     */
    public boolean validateToken(String token, UserDetails userDetails){
        String username = this.getUserNameFromToken(token);
        if (StringUtils.isEmpty(username)) {
            return false;
        }
        if (username.equals(userDetails.getUsername()) && this.isTokenExpired(token)) {
            return true;
        }

        return false;
    }

    /**
     * 验证token是否过期
     * @param token
     * @return
     */
    public boolean isTokenExpired(String token){
        Claims claims = this.getClaimsFromToken(token);
        Date expired = claims.getExpiration();
        return new Date().before(expired);
    }

    /**
     * 刷新token
     * @param token
     * @return
     */
    public String refreshToken(String token){
        if (!this.isTokenExpired(token)) {
            logger.info("token已过期:{}",token);
            return null;
        } else {
            Claims claims = this.getClaimsFromToken(token);
            claims.replace(CLAIM_KEY_CREATED,new Date());
            return this.generateTokenByClaims(claims);
        }
    }


}

  4、在通用包common下新建未登录时返回结果类RestAuthenestAuthenticationEntryPoint

package com.zzb.test.admin.common;

import cn.hutool.json.JSONUtil;
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失效访问接口时,自定义的返回结果
 * Created by zzb on 2019/11/21 14:22
 */
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(CommonResult.failed(e.getMessage())));
        response.getWriter().flush();
    }
}

  在通用包common下新建无权访问时返回结果类RestfulAccessDeinidccessDeniedHandler

package com.zzb.test.admin.common;

import cn.hutool.json.JSONUtil;
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;

/**
 * 当访问接口没有权限时,自定义的返回结果
 * Created by zzb on 2019/11/21 14:15
 */
@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.forbbiden(e.getMessage())));
        response.getWriter().flush();
    }
}

  在通用包common下新建JWT登录授权过滤器JwtAuthenticationTokenFilter

package com.zzb.test.admin.common;

import com.zzb.test.admin.utils.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;

/**
 * JWT登录授权过滤器
 * Created by zzb on 2019/11/21 14:31
 */
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);
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            String authToken = authHeader.substring(this.tokenHead.length());
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            logger.info("解析token获取到用户名:{}",username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(authToken,userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    logger.info("给用户授权:{}",username);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }

        chain.doFilter(request,response);
    }
}

  5、在service包下新建UmsAdminService接口

package com.zzb.test.admin.service;

import com.zzb.test.admin.mbg.model.UmsAdmin;
import com.zzb.test.admin.mbg.model.UmsPermission;

import java.util.List;

/**
 * 用户管理Service
 * Created by zzb on 2019/11/21 17:33
 */
public interface UmsAdminService {
    /**
     * 根据用户名查询用户
     * @param username
     * @return
     */
    UmsAdmin getAdminByUsername(String username);

    /**
     * 用户注册
     * @param umsAdminParam
     * @return
     */
    UmsAdmin register(UmsAdmin umsAdminParam);

    /**
     * 用户登录
     * @param username
     * @param password
     * @return
     */
    String login(String username,String password);

    /**
     * 根据用户id查询用户权限
     * @param adminId
     * @return
     */
    List<UmsPermission> getPermissionList(Long adminId);
}

  在impl包下新建其实现类UmsAdminServiceImpl

package com.zzb.test.admin.service.impl;

import cn.hutool.core.lang.Assert;
import com.zzb.test.admin.dao.UmsAdminRoleRelationDao;
import com.zzb.test.admin.mbg.mapper.UmsAdminMapper;
import com.zzb.test.admin.mbg.model.UmsAdmin;
import com.zzb.test.admin.mbg.model.UmsAdminExample;
import com.zzb.test.admin.mbg.model.UmsPermission;
import com.zzb.test.admin.service.UmsAdminService;
import com.zzb.test.admin.utils.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.util.Date;
import java.util.List;

/**
 * 用户管理Service的impl
 * Created by zzb on 2019/11/21 17:41
 */
@Service
@Transactional
public class UmsAdminServiceImpl implements UmsAdminService {

    private static final Logger logger = LoggerFactory.getLogger(UmsAdminServiceImpl.class);
    @Autowired
    private UmsAdminMapper umsAdminMapper;
    @Autowired
    private UmsAdminRoleRelationDao umsAdminRoleRelationDao;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    public UmsAdmin getAdminByUsername(String username) {
        UmsAdminExample uae = new UmsAdminExample();
        uae.createCriteria().andUsernameEqualTo(username)
                .andDelStatusEqualTo("0")
                .andStatusEqualTo(true);
        List<UmsAdmin> admins = umsAdminMapper.selectByExample(uae);
        Assert.notNull(admins,"用户名不存在");
        return admins.get(0);
    }

    @Override
    public List<UmsPermission> getPermissionList(Long adminId) {
        return umsAdminRoleRelationDao.getPermissionList(adminId);
    }

    @Override
    public UmsAdmin register(UmsAdmin umsAdminParam) {
        //是否有相同用户名
        UmsAdminExample uae = new UmsAdminExample();
        uae.createCriteria().andUsernameEqualTo(umsAdminParam.getUsername())
                .andDelStatusEqualTo("0");
        List list = umsAdminMapper.selectByExample(uae);
        if (!CollectionUtils.isEmpty(list)) {
            return null;
        }

        umsAdminParam.setStatus(true);
        umsAdminParam.setDelStatus("0");
        umsAdminParam.setCreateTime(new Date());
        umsAdminParam.setModifyTime(new Date());
        //将密码进行加密
        String encodePassword = passwordEncoder.encode(umsAdminParam.getPassword());
        umsAdminParam.setPassword(encodePassword);
        if (umsAdminMapper.insert(umsAdminParam)<1) {
            return null;
        };
        return umsAdminParam;
    }

    @Override
    public String login(String username, String password) {
        String token = null;
        try {
            //校验用户名,封装用户实体
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            //检验密码
            if (!passwordEncoder.matches(password,userDetails.getPassword())) {
                throw new BadCredentialsException("密码错误");
            }
            //根据正确的用户名密码生成token
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
            token = jwtTokenUtil.generateToken(userDetails);
        } catch (AuthenticationException e) {
            logger.warn("登录异常{}",e.getMessage());
        }

        return token;
    }
}

  需要自定义写sql语句获取权限,新建dao包,并在dao包下新建UmsAdminRoleRelationDao

package com.zzb.test.admin.dao;

import com.zzb.test.admin.mbg.model.UmsPermission;

import java.util.List;

/**
 * 用户与角色关系自定义dao
 * Created by zzb on 2019/11/21 18:00
 */
public interface UmsAdminRoleRelationDao {
    /**
     * 获取用户的所有权限
     * @param adminId
     * @return
     */
    List<UmsPermission> getPermissionList(Long adminId);
}

  新建对应的xml文件UmsAdminRoleRelationDao.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzb.test.admin.dao.UmsAdminRoleRelationDao">
  <select id="getPermissionList" resultMap="com.zzb.test.admin.mbg.mapper.UmsPermissionMapper.BaseResultMap" parameterType="java.lang.Long">
    SELECT
      p.*
    FROM
      ums_admin_role_relation ar
    LEFT JOIN ums_role r ON ar.role_id = r.id
    LEFT JOIN ums_role_permission_relation rp ON r.id = rp.role_id
    LEFT JOIN ums_permission p ON rp.permission_id = p.id
    WHERE
      ar.admin_id = #{adminId}
    AND p.id IS NOT NULL
    AND p.id NOT IN (
      SELECT
        p.id
      FROM
        ums_admin_permission_relation pr
      LEFT JOIN ums_permission p ON pr.permission_id = p.id
      WHERE 1=1
      AND pr.admin_id = #{adminId}
    )
    UNION
    SELECT
      p.*
    FROM
      ums_admin_permission_relation pr
    LEFT JOIN ums_permission p ON pr.permission_id = p.id
    WHERE 1=1
    AND pr.admin_id = #{adminId}
  </select>
</mapper>

  修改mybatis的映射配置类MybatisConfig

package com.zzb.test.admin.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

/**
 * Created by zzb on 2019/11/15 9:36
 */
@Configuration
@MapperScan(basePackages = {"com.zzb.test.admin.mbg.mapper","com.zzb.test.admin.dao"})
public class MybatisConfig {
}

  6、新建自定义实体包dto,并新建SpringSecurity需要的用户实体AdminUserDetails

package com.zzb.test.admin.dto;

import com.zzb.test.admin.mbg.model.UmsAdmin;
import com.zzb.test.admin.mbg.model.UmsPermission;
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需要的用户详情
 * Created by zzb on 2019/11/21 18:22
 */
public class AdminUserDetails implements UserDetails {

    private UmsAdmin umsAdmin;
    private List<UmsPermission> permissionList;

    public AdminUserDetails(UmsAdmin umsAdmin,List<UmsPermission> permissionList){
        this.umsAdmin = umsAdmin;
        this.permissionList = permissionList;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //返回当前用户的权限
        return permissionList.stream()
                .filter(permission -> permission.getPerValue()!=null)
                .map(permission -> new SimpleGrantedAuthority(permission.getPerValue()))
                .collect(Collectors.toList());
    }

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

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

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

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

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

    @Override
    public boolean isEnabled() {
        return umsAdmin.getStatus();
    }
}

  7、在config包下新建SpringSecurity的配置类SecturyConfig

package com.zzb.test.admin.config;

import com.zzb.test.admin.common.JwtAuthenticationTokenFilter;
import com.zzb.test.admin.common.RestAuthenticationEntryPoint;
import com.zzb.test.admin.common.RestfulAccessDeniedHandler;
import com.zzb.test.admin.dto.AdminUserDetails;
import com.zzb.test.admin.mbg.model.UmsAdmin;
import com.zzb.test.admin.mbg.model.UmsPermission;
import com.zzb.test.admin.service.UmsAdminService;
import io.jsonwebtoken.lang.Assert;
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.authentication.builders.AuthenticationManagerBuilder;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.util.List;

/**
 * SpringSecurity的配置类
 * Created by zzb on 2019/11/21 13:37
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UmsAdminService umsAdminService;
    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    /**
     * 配置需要拦截的url路径、jwt过滤器及出异常后的处理器
     * @param httpSecurity
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception{
        httpSecurity.csrf()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.GET,        //允许对于网站静态资源的无授权访问
                        "/", "/*.html","favicon.icon","/**/*.html","/**/*.css","/**/*.js",
                        "/swagger-resources/**","/v2/api-docs/**")
                .permitAll()
                .antMatchers("/admin/login","/admin/register")      //对登录注册允许匿名访问
                .permitAll()
                .antMatchers(HttpMethod.OPTIONS)        //跨域请求会先进行一次options请求
                .permitAll()
//                .antMatchers("/**")     //测试时全部放开
//                .permitAll()
                .anyRequest()       //除上面外的所有请求全部需要鉴权认证
                .authenticated();
        //禁用缓存
        httpSecurity.headers().cacheControl();
        //添加JWT 过滤器filter
        httpSecurity.addFilterBefore(this.jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        //添加自定义未授权和未登录结果返回
        httpSecurity.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);
    }

    /**
     * 在用户名和密码校验前添加的过滤器,如果有token信息,会自行根据token信息进行登录
     * @return
     * @throws Exception
     */
    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() throws Exception{
        return new JwtAuthenticationTokenFilter();
    }

    /**
     * 配置UserDetailsService 和 PasswordEncoder
     * @param auth
     * @throws Exception
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception{
        auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }

    /**
     * SpringSecurty编码比对密码
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService(){
        //获取登录用户信息
        return username->{
            UmsAdmin umsAdmin = umsAdminService.getAdminByUsername(username);
            Assert.notNull(umsAdmin,"用户名不存在!");
            List<UmsPermission> permissionList = umsAdminService.getPermissionList(umsAdmin.getId());
            return new AdminUserDetails(umsAdmin,permissionList);
        };
    }

}

七、登录与注册功能的实现

  1、在controller包下新建UmsAdminController类

package com.zzb.test.admin.controller;

import cn.hutool.core.lang.Assert;
import com.zzb.test.admin.common.CommonResult;
import com.zzb.test.admin.mbg.model.UmsAdmin;
import com.zzb.test.admin.mbg.model.UmsPermission;
import com.zzb.test.admin.service.UmsAdminService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

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

/**
 * 用户管理Controller
 * Created by zzb on 2019/11/22 9:37
 */
@Api(tags = "UmsAdminController", description = "用户管理")
@Controller
public class UmsAdminController {
    @Autowired
    private UmsAdminService umsAdminService;
    @Value("${jwt.tokenHead}")
    private String tokenHead;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;

    /**
     * 用户注册
     * @param umsAdminParam
     * @return
     */
    @ApiOperation("用户注册")
    @ResponseBody
    @RequestMapping(value = "/admin/register", method = RequestMethod.POST)
    public CommonResult register(@RequestBody UmsAdmin umsAdminParam){
        String originPassword = umsAdminParam.getPassword();
        //注册用户账号
        UmsAdmin umsAdmin = umsAdminService.register(umsAdminParam);
        Assert.notNull(umsAdmin,"用户注册失败");
        //注册成功后自动登录
        String token = umsAdminService.login(umsAdminParam.getUsername(),originPassword);
        if (StringUtils.isEmpty(token)) {
            return CommonResult.failed("自动登录失败!!");
        }
        Map<String,String> tokenMap = new HashMap<>();
        tokenMap.put("token",token);
        tokenMap.put("tokenHead", tokenHead);

        return CommonResult.success(tokenMap,"自动登录成功!!");
    }

    /**
     * 用户登录
     * @param umsAdminParam
     * @return
     */
    @ApiOperation("用户登录")
    @ResponseBody
    @RequestMapping(value = "/admin/login", method = RequestMethod.POST)
    public CommonResult login(@RequestBody UmsAdmin umsAdminParam){
        //根据输入的用户名和密码生成token
        String token = umsAdminService.login(umsAdminParam.getUsername(),umsAdminParam.getPassword());
        if (StringUtils.isEmpty(token)) {
            return CommonResult.failed("用户名或密码错误!!");
        }
        Map<String,String> tokenMap = new HashMap<>();
        tokenMap.put("token",token);
        tokenMap.put("tokenHead", tokenHead);

        return CommonResult.success(tokenMap);
    }

    /**
     * 获取用户所有的权限
     * @param adminId
     * @return
     */
    @ApiOperation("获取用户所有权限")
    @ResponseBody
    @RequestMapping(value = "/admin/getPermissionList/{adminId}", method = RequestMethod.POST)
    public CommonResult<List<UmsPermission>> getPermissionList(@PathVariable Long adminId){
        List<UmsPermission> permissionList = umsAdminService.getPermissionList(adminId);
        return CommonResult.success(permissionList);
    }


}

  2、修改SwaggerConfig类,通过修改swagger-ui配置实现调用接口自带Authorization头,这样就可以访问需要登录的接口了

package com.zzb.test.admin.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

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

/**
 * Swagger-UI的配置类
 * Created by zzb on 2019/11/18 18:14
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createApi(){
        return new Docket(DocumentationType.SWAGGER_2)
                //文档head主体
                .apiInfo(this.apiInfo())
                //生成Controller的api文档
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.zzb.test.admin.controller"))
                //为有@Api注解的Controller生成API文档
//                .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
                //为有@ApiOperation注解的方法生成API文档
//                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build()
                //添加登录认证
                .securitySchemes(this.securitySchemes())
                .securityContexts(this.securityContexts());
    }

    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("Swagger-UI演示")
                .description("shop-test")
                .version("1.0.0")
                .build();
    }

    private List<ApiKey> securitySchemes(){
        //设置请求头信息
        List<ApiKey> result = new ArrayList<>();
        ApiKey apiKey = new ApiKey("Authorization","Authorization","header");
        result.add(apiKey);
        return result;
    }

    private List<SecurityContext> securityContexts(){
        //设置需要登录认证的路径
        List<SecurityContext> result = new ArrayList<>();
        result.add(this.getContextByPath("/admin/brand/.*"));
        return result;
    }

    private SecurityContext getContextByPath(String pathRegex){
        return SecurityContext.builder()
                .securityReferences(this.defaultAuth())
                .forPaths(PathSelectors.regex(pathRegex))
                .build();
    }

    private List<SecurityReference> defaultAuth(){
        List<SecurityReference> result = new ArrayList<>();
        AuthorizationScope authorizationScope = new AuthorizationScope("global","全部通过");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        result.add(new SecurityReference("Authorization", authorizationScopes));
        return result;
    }
}

  3、给PmsBrandController接口中的方法添加访问权限

  · 给查询接口添加pms:brand:read权限

@ApiOperation("分页获取所有品牌")
    @RequestMapping(value = "/admin/brand/getList", method = RequestMethod.GET)
    @ResponseBody
    @PreAuthorize("hasAuthority('pms:brand:read')")
    public CommonResult<CommonPage<PmsBrand>> getList(@RequestParam(value = "pageNum", defaultValue = "1") @ApiParam("分页页码") Integer pageNum,
                                                           @RequestParam(value = "pageSize", defaultValue = "10") @ApiParam("每页数量") Integer pageSize){
        List<PmsBrand> list = pmsBrandService.getList(pageNum,pageSize);
        logger.info("分页查询所有品牌==》" + list);
        return CommonResult.success(CommonPage.restPage(list));
    }

  · 给修改接口添加pms:brand:update权限

@ApiOperation("添加品牌")
    @RequestMapping(value = "/admin/brand/insert", method = RequestMethod.POST)
    @ResponseBody
    @PreAuthorize("hasAuthority('pms:brand:create')")
    public CommonResult insert(@ApiParam("品牌信息") PmsBrand pmsBrand){
        int count = pmsBrandService.insert(pmsBrand);
        logger.info("添加品牌==》" + count);
        if (count>0) {
            return CommonResult.success("添加品牌成功");
        }
        return CommonResult.failed();
    }

  · 给删除接口添加pms:brand:delete权限

@ApiOperation("删除品牌")
    @RequestMapping(value = "/admin/brand/delete", method = RequestMethod.POST)
    @ResponseBody
    @PreAuthorize("hasAuthority('pms:brand:delete')")
    public CommonResult delete(@ApiParam("品牌id") Long id){
        int count = pmsBrandService.delete(id);
        logger.info("删除品牌==》" + count);
        if (count>0) {
            return CommonResult.success("删除品牌成功");
        }
        return CommonResult.failed();
    }

  · 给添加接口添加pms:brand:create权限

@ApiOperation("更新品牌")
    @RequestMapping(value = "/admin/brand/update", method = RequestMethod.POST)
    @ResponseBody
    @PreAuthorize("hasAuthority('pms:brand:update')")
    public CommonResult update(@ApiParam("修改主体") PmsBrand pmsBrand){
        int count = pmsBrandService.update(pmsBrand);
        logger.info("更新品牌==》" + count);
        if (count>0) {
            return CommonResult.success("更新品牌成功");
        }
        return CommonResult.failed();
    }

八、测试与验证

  1、未登录直接访问/admin/brand/getList接口,分页获取所有品牌

  

   

  2、注册新用户,访问/admin/register接口

  

   

  查看数据库表ums_admin,有test用户存在,id为8

   3、未赋予权限,登录后直接访问品牌/admin/brand/getList接口,分页获取所有品牌

  swagger登录设置token

  

   访问接口

  

 

   

   4、给test用户赋予相应的访问权限

  配置权限表,向ums_permission表添加5条数据

   给id为8的test用户设置读的权限

  5、赋予权限后再次访问接口/admin/brand/getList

  

 

  项目github地址:https://github.com/18372561381/shoptest

posted @ 2019-11-21 19:02  不浪小生  阅读(2948)  评论(0编辑  收藏  举报