• 电商项目实战(架构四)——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

  • 相关阅读:
    指针数组和数组指针
    initializer_list
    main:处理命令行选项
    Synchronized 和Lock区别
    sleep和wait的区别
    什么时候会发生类初始化
    类的加载与ClassLoader的理解
    获取Class类的实例
    元注解
    IO流思维导图
  • 原文地址:https://www.cnblogs.com/zzb-yp/p/11899880.html
Copyright © 2020-2023  润新知