SpringBoot整合SpringSecurity

一、准备相关工具类
  1. 导入依赖
        
        
            org.springframework.boot
            spring-boot-starter-security
        
        
        
            io.jsonwebtoken
            jjwt
        
        
        
            javax.xml.bind
            jaxb-api
            2.3.0
        
        
            com.sun.xml.bind
            jaxb-impl
            2.3.0
        
        
            com.sun.xml.bind
            jaxb-core
            2.3.0
        
        
            javax.activation
            activation
            1.1.1
        
  1. IpUtil工具类
package com.xxz.common.utils;

import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * 获取ip地址
 */
public class IpUtil {

    public static String getIpAddress(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
                if (ipAddress.equals("127.0.0.1")) {
                    // 根据网卡取本机配置的IP
                    InetAddress inet = null;
                    try {
                        inet = InetAddress.getLocalHost();
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    }
                    ipAddress = inet.getHostAddress();
                }
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress="";
        }
        // ipAddress = this.getRequest().getRemoteAddr();

        return ipAddress;
    }

    public static String getGatwayIpAddress(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String ip = headers.getFirst("x-forwarded-for");
        if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
            // 多次反向代理后会有多个ip值,第一个ip才是真实ip
            if (ip.indexOf(",") != -1) {
                ip = ip.split(",")[0];
            }
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddress().getAddress().getHostAddress();
        }
        return ip;
    }
}
  1. JwtHelper工具类
package com.xxz.common.utils;

import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;

import java.util.Date;

/**
 * 生成JSON Web令牌的工具类
 */
public class JwtHelper {

    //token过期时间
    private static long tokenExpiration = 365 * 24 * 60 * 60 * 1000;
    //加密秘钥
    private static String tokenSignKey = "1234567890";

    //根据用户id和用户名称生成token字符串
    public static String createToken(String userId, String username) {
        String token = Jwts.builder()
                .setSubject("AUTH-USER") //设置主题
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) //设置过期时间
                .claim("userId", userId) //有效载荷
                .claim("username", username) //有效载荷
                .signWith(SignatureAlgorithm.HS512, tokenSignKey) //HS512集成自定义密钥方式加密
                .compressWith(CompressionCodecs.GZIP) //字符串压缩处理
                .compact();
        return token;
    }

    //从token字符串获取userid
    public static String getUserId(String token) {
        try {
            if (StringUtils.isEmpty(token)) return null;
            // Jwt解析(parse) , 通过密钥(tokenSignKey) 进行
            Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims claims = claimsJws.getBody(); //获取载荷主体部分
            String userId = (String) claims.get("userId"); //获取有效信息
            return userId;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    //从token字符串获取username
    public static String getUsername(String token) {
        try {
            if (StringUtils.isEmpty(token)) return "";

            Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims claims = claimsJws.getBody();
            return (String) claims.get("username");
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    //无需定义token删除,客户端扔掉即可

    public static void main(String[] args) {
        String token = JwtHelper.createToken("1", "test");
        System.out.println(token);

        String userId = JwtHelper.getUserId(token);
        System.out.println(userId);

        String username = JwtHelper.getUsername(token);
        System.out.println(username);
    }
}
  1. MD5工具类
package com.xxz.common.utils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;


public final class MD5 {

    public static String encrypt(String strSrc) {
        try {
            char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f' };
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }


}

  1. ResponseUtil响应工具类
package com.xxz.common.utils;

import com.xxz.common.result.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

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

public class ResponseUtil {

    public static void out(HttpServletResponse response, Result r) {
        ObjectMapper mapper = new ObjectMapper();
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        try {
            mapper.writeValue(response.getWriter(), r);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
二、认证

SpringBoot整合SpringSecurity_第1张图片

  1. 自定义密码组件 MD5
package com.xxz.system.custom;

import com.xxz.common.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

//一、自定义密码处理组件
@Component
public class CustomMD5Password implements PasswordEncoder {

    @Override
    public String encode(CharSequence rawPassword) {
        //进行MD5加密
        return MD5.encrypt(rawPassword.toString());
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodePassword) {
        //判断是否相等
        return encodePassword.equals(MD5.encrypt(rawPassword.toString()));
    }
}

  1. 自定义用户对象
package com.xxz.system.custom;

import com.xxz.model.system.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

//二、自定义用户类(继承Security里的User类)
public class CustomUser extends User {

    /**
     * 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象
     */
    private SysUser sysUser;

    public CustomUser(SysUser sysUser, Collection authorities){
        //调用父类构造器初始化信息
        super(sysUser.getUsername(), sysUser.getPassword(), authorities);
        this.sysUser = sysUser;
    }

    public SysUser getSysUser() {
        return sysUser;
    }

    public void setSysUser(SysUser sysUser) {
        this.sysUser = sysUser;
    }

    @Override
    public String toString() {
        return "CustomUser{" +
                "sysUser=" + sysUser +
                '}';
    }
}

  1. 自定义业务查询方法
package com.xxz.system.service;

import com.xxz.model.system.SysUser;
import com.xxz.system.custom.CustomUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.Collections;

//三、自定义根据前端数据的用户参数-获取用户数据的业务类及业务查询方法,默认时从内存中获取查询用户信息(需要在Security配置类中配置内存相关存储用户信息)
@Component
public class UserDetailServiceImpl implements UserDetailsService {

    //定义自己的业务类,用于查询数据库信息
    @Autowired
    private SysUserService sysUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //调用业务类查询数据库
        SysUser sysUser = sysUserService.getUserInfoByUserName(username);
        //判断用户是否为空
        if(null == sysUser){
            throw new UsernameNotFoundException("用户不存在");
        }
        //判断用户状态是否可用
        if(sysUser.getStatus().intValue() == 0){
            throw new RuntimeException("用户已禁用...");
        }
        //返回自定义角色对象信息
        return new CustomUser(sysUser, Collections.emptyList());
    }
}

  1. 自定义认证过滤器
package com.xxz.system.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.xxz.common.result.Result;
import com.xxz.common.result.ResultCodeEnum;
import com.xxz.common.utils.JwtHelper;
import com.xxz.common.utils.ResponseUtil;
import com.xxz.model.vo.LoginVo;
import com.xxz.system.custom.CustomUser;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

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

/**
 * 登入过滤器,继承UserNamePasswordAuthenticationFilter , 对用户名密码进行登录校验
 */

//四、自定义认证过滤器 //继承               UsernamePasswordAuthenticationFilter
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    //定义构造器用于做当前登录过滤器初始化
    public TokenLoginFilter(AuthenticationManager authenticationManager){
        //设置/初始化  认证管理器
        this.setAuthenticationManager(authenticationManager);
        //取消 只针对post请求
        this.setPostOnly(false);
        //指定登录接口及提交方式,可以指定任意路径
        this.setRequiresAuthenticationRequestMatcher(
                new AntPathRequestMatcher("/admin/system/index/login", "POST"));
    }

    //重写获取用户信息方法
    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {

        try {
            //以流的方式获取前端请求接口封装的用户对象
            LoginVo loginVo = new ObjectMapper().readValue(request.getInputStream(), LoginVo.class);
            //创建UsernamePasswordAuthenticationToken对象,封装用户名和密码,得到认证对象
            Authentication authenticationToken =
                    new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
            //返回目标认证对象
            Authentication authentication = this.getAuthenticationManager().authenticate(authenticationToken);
            System.out.println("authenticate ===== " + authentication);
            return authentication;
        }catch (IOException e){
            e.printStackTrace();
        }
        return null;
    }

    //重写登录成功调用/执行
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        //auth : 表示当前认证对象
        //1.获取认证对象
        CustomUser customUser = (CustomUser) auth.getPrincipal();
        System.out.println("===========================================================");
        System.out.println("===========================================================");
        System.out.println("===========================================================");
        System.out.println(" auth.getDetails(); : ==== " +  auth);
        System.out.println("===========================================================");
        System.out.println("===========================================================");
        System.out.println("===========================================================");
        //2.生成token
        String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
        //3.返回(通过响应工具)
        Map map = new HashMap<>();
        map.put("token", token);
        ResponseUtil.out(response, Result.ok(map));
    }

    //重写登录失败调用/执行
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request,
                                              HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {
       //判断当前异常是否属于运行时异常
        if(e.getCause() instanceof RuntimeException){
            ResponseUtil.out(response, Result.build(null, 204, e.getMessage()));
        }else{
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
        }

    }


}

  1. 自定义同一返回结果 Result and ResultCodeEnum
package com.xxz.common.result;

import lombok.Data;

/**
 * 统一放回结果  [五、自定义同一返回结果 Result and ResultCodeEnum]
 * @param 
 */
@Data
public class Result {

    //返回码
    private Integer code;

    //返回消息
    private String message;

    //返回数据
    private T data;

    public Result(){}

    // 返回数据
    protected static  Result build(T data) {
        Result result = new Result();
        if (data != null)
            result.setData(data);
        return result;
    }

    public static  Result build(T body, Integer code, String message) {
        Result result = build(body);
        result.setCode(code);
        result.setMessage(message);
        return result;
    }

    public static  Result build(T body, ResultCodeEnum resultCodeEnum) {
        Result result = build(body);
        result.setCode(resultCodeEnum.getCode());
        result.setMessage(resultCodeEnum.getMessage());
        return result;
    }

    public static Result ok(){
        return Result.ok(null);
    }

    /**
     * 操作成功
     * @param data  baseCategory1List
     * @param 
     * @return
     */
    public static Result ok(T data){
        Result result = build(data);
        return build(data, ResultCodeEnum.SUCCESS);
    }

    public static Result fail(){
        return Result.fail(null);
    }

    /**
     * 操作失败
     * @param data
     * @param 
     * @return
     */
    public static Result fail(T data){
        Result result = build(data);
        return build(data, ResultCodeEnum.FAIL);
    }

    public Result message(String msg){
        this.setMessage(msg);
        return this;
    }

    public Result code(Integer code){
        this.setCode(code);
        return this;
    }
}

package com.xxz.common.result;

import lombok.Getter;

@Getter
public enum ResultCodeEnum {

    SUCCESS(200,"成功"),
    FAIL(201, "失败"),
    SERVICE_ERROR(2012, "服务异常"),
    DATA_ERROR(204, "数据异常"),
    ILLEGAL_REQUEST(205, "非法请求"),
    REPEAT_SUBMIT(206, "重复提交"),
    ARGUMENT_VALID_ERROR(210, "参数校验异常"),

    LOGIN_AUTH(208, "未登陆"),
    PERMISSION(209, "没有权限"),
    ACCOUNT_ERROR(214, "账号不正确"),
    PASSWORD_ERROR(215, "密码不正确"),
    LOGIN_MOBLE_ERROR( 216, "账号不正确"),
    ACCOUNT_STOP( 217, "账号已停用"),
    NODE_ERROR( 218, "该节点下有子节点,不可以删除")
    ;

    private Integer code;

    private String message;

    private ResultCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

  1. 自定义认证解析过滤器
package com.xxz.system.filter;

import com.xxz.common.result.Result;
import com.xxz.common.result.ResultCodeEnum;
import com.xxz.common.utils.JwtHelper;
import com.xxz.common.utils.ResponseUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
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;
import java.util.Collections;

//六、自定义认证解析过滤器
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    public TokenAuthenticationFilter(){

    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        logger.info("uri: " + request.getRequestURI());
        //如果时登录接口,直接放行(每个项目的登录接口不同)
        if("/admin/system/index/login".equals(request.getRequestURI())){
            //放行
            filterChain.doFilter(request, response);
            //停止运行
            return;
        }
        //调用自定义方法,获取用户信息认证对象
        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        //如果不为空,则存储到SecurityContextHolder对象中
        if(null != authentication){
            SecurityContextHolder.getContext().setAuthentication(authentication);
            //放行
            filterChain.doFilter(request, response);
        }else{
            //如果为空,则返回结果信息
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
        }

    }

    //定义获取用户信息认证对象,通过token获取
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){

        //token置于header里面
        String token = request.getHeader("token");
        logger.info("token: " + token);
        //token不为空则获取token中的用户信息
        if(!StringUtils.isEmpty(token)){
            String username = JwtHelper.getUsername(token);
            logger.info("username: " + username);
            //如果用户名不为空则返回目标认证对象
            if(!StringUtils.isEmpty(username)){
                return new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList());
            }
        }

        return null;
    }
}

  1. Security核心配置类
package com.xxz.system.config;

import com.xxz.system.custom.CustomMD5Password;
import com.xxz.system.filter.TokenAuthenticationFilter;
import com.xxz.system.filter.TokenLoginFilter;
import com.xxz.system.service.UserDetailServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
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.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * 六、配置SpirngSecurity配置类(自定义大整合)
 */
@Configuration
@EnableWebSecurity //开启SpringSecurity的默认行为
@Slf4j //日志
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    //导入自己编写的查询业务类(默认是providerService去内存查找)
    @Autowired
    private UserDetailServiceImpl userDetailsService;

    //导入我们自己编写的加密工具类
    @Autowired
    private CustomMD5Password customMD5Password;

    /**
     *  用户认证管理器
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    protected AuthenticationManager authenticationManager()throws Exception{
        log.info("AuthenticationManager用户认证管理器 + 加入容器.......");
        log.info("当前注入对象UserDetailsService : ===== " + userDetailsService);
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http)throws Exception{
        //这是配置的关键,决定那些接口开启防护,那些接口绕过防护
        http
                //关闭csrf
                .csrf().disable()
                //开启跨域以便前端调用接口
                .cors()
                .and()
                .authorizeRequests()
                //指定某些接口不需要通过认证即可访问, 登录接口肯定是不需要的
                .antMatchers("/admin/system/index/login").permitAll()
                //这里的意思是其他所有接口需要认证才能访问
                .anyRequest().authenticated()
                .and()
                //TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面
                .addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilter(new TokenLoginFilter(authenticationManager()));

        //禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        //指定UserDetailService和加密器(使用自定义的)
        log.info("指定UserDetailService和加密器(使用自定义的) + 加入容器.......");
        auth.userDetailsService(userDetailsService).passwordEncoder(customMD5Password);
    }

    /**
     * 配置那些请求不拦截
     * 排除swagger 相关请求
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web)throws Exception{
        web.ignoring().antMatchers(
                "/favicon.ico",
                "/swagger-resources/**",
                "/webjars/**", "/v2/**",
                "/swagger-ui.html",
                "/doc.html");
    }

}

三、授权

SpringBoot整合SpringSecurity_第2张图片

  1. 修改loadUserByUsername查询用户权限操作数据返回
package com.xxz.system.service;

import com.xxz.model.system.SysMenu;
import com.xxz.model.system.SysUser;
import com.xxz.model.vo.RouterVo;
import com.xxz.system.custom.CustomUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

//三、自定义根据前端数据的用户参数-获取用户数据的业务类及业务查询方法,默认时从内存中获取查询用户信息(需要在Security配置类中配置内存相关存储用户信息)
@Component
public class UserDetailServiceImpl implements UserDetailsService {

    //定义自己的业务类,用于查询数据库信息
    @Autowired
    private SysUserService sysUserService;

    //注入菜单模块业务
    @Autowired
    private SysMenuService sysMenuService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //调用业务类查询数据库
        SysUser sysUser = sysUserService.getUserInfoByUserName(username);
        //判断用户是否为空
        if(null == sysUser){
            throw new UsernameNotFoundException("用户不存在");
        }
        //判断用户状态是否可用
        if(sysUser.getStatus().intValue() == 0){
            throw new RuntimeException("用户已禁用...");
        }
        //返回自定义角色对象信息
        //[授权 new :根据用户userid查询操作权限数据(获取当前用户所有->权限操作符)]
        List userPermsList = sysMenuService.findUserButtonListByUserId(sysUser.getId());
        //转换成Security要求的格式数据
        List authorities = new ArrayList<>();
        //遍历所有权限操作符
        for (String perm : userPermsList) {
            authorities.add(new SimpleGrantedAuthority(perm.trim()));
        }
        // [old return]
//        return new CustomUser(sysUser, Collections.emptyList());
        //[new return ]
        return new CustomUser(sysUser, authorities);
    }
}

  1. 配置Redis,存储当前用户权限数据SpringSecurity配置(将权限数据封装成SpringSecurity中的Authentication类)
  • [引入redis依赖到我们的security项目中]
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
  • [application.yml配置redis服务配置]
# server
server:
  port: 8800

# mp
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #查看日志
  mapper-locations: classpath:mapper/*.xml

# spring
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/guigu-auth?characterEncoding=utf-8&useSSL=false&zeroDateTimeBehavior=convertToNull
    username: root
    password: root

# 日期格式问题
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

# redis
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    timeout: 1800000
    password:
    jedis:
      pool:
        max-active: 20 #最大连接数
        max-wait: -1 #最大阻塞等待时间(负数表示没有限制)
        max-idle: 5 #最大空闲
        min-idle: 0 #最小空闲
  • [注意:Linux要修改配置文件-支持远程连接]
    SpringBoot整合SpringSecurity_第3张图片
  • [指定配置文件启动]
    SpringBoot整合SpringSecurity_第4张图片
  1. 修改过滤器

3.1认证过滤器

package com.xxz.system.filter;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xxz.common.result.Result;
import com.xxz.common.result.ResultCodeEnum;
import com.xxz.common.utils.JwtHelper;
import com.xxz.common.utils.ResponseUtil;
import com.xxz.model.vo.LoginVo;
import com.xxz.system.custom.CustomUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

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

/**
 * 登入过滤器,继承UserNamePasswordAuthenticationFilter , 对用户名密码进行登录校验
 */

//四、自定义认证过滤器 //继承               UsernamePasswordAuthenticationFilter
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    //[new 授权]
    private RedisTemplate redisTemplate;

    //定义构造器用于做当前登录过滤器初始化 [new 授权]
    public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate){
        //设置/初始化  认证管理器
        this.setAuthenticationManager(authenticationManager);
        //取消 只针对post请求
        this.setPostOnly(false);
        //指定登录接口及提交方式,可以指定任意路径
        this.setRequiresAuthenticationRequestMatcher(
                new AntPathRequestMatcher("/admin/system/index/login", "POST"));
        //[new 授权]
        this.redisTemplate = redisTemplate;
    }

    //重写获取用户信息方法
    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {

        try {
            //以流的方式获取前端请求接口封装的用户对象
            LoginVo loginVo = new ObjectMapper().readValue(request.getInputStream(), LoginVo.class);
            //创建UsernamePasswordAuthenticationToken对象,封装用户名和密码,得到认证对象
            Authentication authenticationToken =
                    new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
            //返回目标认证对象
            Authentication authentication = this.getAuthenticationManager().authenticate(authenticationToken);
            System.out.println("authenticate ===== " + authentication);
            return authentication;
        }catch (IOException e){
            e.printStackTrace();
        }
        return null;
    }

    //重写登录成功调用/执行
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        //auth : 表示当前认证对象
        //1.获取认证对象
        CustomUser customUser = (CustomUser) auth.getPrincipal();
        System.out.println("===========================================================");
        System.out.println("===========================================================");
        System.out.println("===========================================================");
        System.out.println(" auth.getDetails(); : ==== " +  auth);
        System.out.println("===========================================================");
        System.out.println("===========================================================");
        System.out.println("===========================================================");

        //[new 授权]:认证成功后,将用户数据存储到redis中 (以用户名称作为key,以Json作为存储数据)
        redisTemplate.opsForValue().set(customUser.getUsername(),
                JSON.toJSONString(customUser.getAuthorities()));

        //2.生成token
        String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
        //3.返回(通过响应工具)
        Map map = new HashMap<>();
        map.put("token", token);
        ResponseUtil.out(response, Result.ok(map));
    }

    //重写登录失败调用/执行
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request,
                                              HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {
       //判断当前异常是否属于运行时异常
        if(e.getCause() instanceof RuntimeException){
            ResponseUtil.out(response, Result.build(null, 204, e.getMessage()));
        }else{
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
        }

    }


}

3.2解析过滤器

...
  1. 在项目中配置redis,完成controller相关代码
package com.xxz.system.filter;

import com.alibaba.fastjson.JSON;
import com.xxz.common.result.Result;
import com.xxz.common.result.ResultCodeEnum;
import com.xxz.common.utils.JwtHelper;
import com.xxz.common.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
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;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

//六、自定义认证解析过滤器
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    //[new 授权]:定义redis
    private RedisTemplate redisTemplate;

    //[new 授权]:引入redis/初始化redis对象
    public TokenAuthenticationFilter(RedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        logger.info("uri: " + request.getRequestURI());
        //如果时登录接口,直接放行(每个项目的登录接口不同)
        if("/admin/system/index/login".equals(request.getRequestURI())){
            //放行
            filterChain.doFilter(request, response);
            //停止运行
            return;
        }
        //调用自定义方法,获取用户信息认证对象
        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        //如果不为空,则存储到SecurityContextHolder对象中
        if(null != authentication){
            SecurityContextHolder.getContext().setAuthentication(authentication);
            //放行
            filterChain.doFilter(request, response);
        }else{
            //如果为空,则返回结果信息
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
        }

    }

    //定义获取用户以及权限信息对象,通过token获取(new 授权:从redis中获取用户信息,包括权限信息)
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){

        //token置于header里面
        String token = request.getHeader("token");
        logger.info("token: " + token);
        //token不为空则获取token中的用户信息
        if(!StringUtils.isEmpty(token)){
            String username = JwtHelper.getUsername(token);
            logger.info("username: " + username);
            //如果用户名不为空则返回目标认证对象
            if(!StringUtils.isEmpty(username)){
                //[new return 授权 ] : 同时获取权限信息
                //根据用户名 key ,从redis中获取当前用户目标权限数据(所有权限操作符)
                String authoritiesString = (String)redisTemplate.opsForValue().get(username);
                //将权限数据转换成map集合(方便操作)
                List mapList = JSON.parseArray(authoritiesString, Map.class);
                //创建Security规定的权限对象集合,将目标权限数据封装成Security指定的
                List authorities = new ArrayList<>();
                for (Map map : mapList) {
                    authorities.add(new SimpleGrantedAuthority((String)map.get("authority")));
                }
                //最总返回目标数据
                return new UsernamePasswordAuthenticationToken(username, null, authorities);
                //[old return]
//                return new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList());
            }
        }

        return null;
    }
}

  1. MySecurityConfig核心配置类也要修改
package com.xxz.system.config;

import com.xxz.system.custom.CustomMD5Password;
import com.xxz.system.filter.TokenAuthenticationFilter;
import com.xxz.system.filter.TokenLoginFilter;
import com.xxz.system.service.UserDetailServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
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.builders.WebSecurity;
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.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * 六、配置SpirngSecurity配置类(自定义大整合)
 */
@Configuration
@EnableWebSecurity //开启SpringSecurity的默认行为
@Slf4j //日志
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启注解功能,默认禁用注解
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    //导入自己编写的查询业务类(默认是providerService去内存查找)
    @Autowired
    private UserDetailServiceImpl userDetailsService;

    //导入我们自己编写的加密工具类
    @Autowired
    private CustomMD5Password customMD5Password;

    //[new 权限]:注入redistemplate依赖
    @Autowired
    private RedisTemplate redisTemplate;

    /*
     *  用户认证管理器
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    protected AuthenticationManager authenticationManager()throws Exception{
        log.info("AuthenticationManager用户认证管理器 + 加入容器.......");
        log.info("当前注入对象UserDetailsService : ===== " + userDetailsService);
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http)throws Exception{
        //这是配置的关键,决定那些接口开启防护,那些接口绕过防护
        http
                //关闭csrf
                .csrf().disable()
                //开启跨域以便前端调用接口
                .cors()
                .and()
                .authorizeRequests()
                //指定某些接口不需要通过认证即可访问, 登录接口肯定是不需要的
                .antMatchers("/admin/system/index/login").permitAll()
                //这里的意思是其他所有接口需要认证才能访问
                .anyRequest().authenticated()
                .and()
                //TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面 [new 权限:注入redistemplate]
                .addFilterBefore(new TokenAuthenticationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)
                .addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate));

        //禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        //指定UserDetailService和加密器(使用自定义的)
        log.info("指定UserDetailService和加密器(使用自定义的) + 加入容器.......");
        auth.userDetailsService(userDetailsService).passwordEncoder(customMD5Password);
    }

    /**
     * 配置那些请求不拦截
     * 排除swagger 相关请求
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web)throws Exception{
        web.ignoring().antMatchers(
                "/favicon.ico",
                "/swagger-resources/**",
                "/webjars/**", "/v2/**",
                "/swagger-ui.html",
                "/doc.html");
    }

}

  1. 通过Controller进行Security权限测试——以角色列表分页查询

SpringBoot整合SpringSecurity_第5张图片
SpringBoot整合SpringSecurity_第6张图片

    //逻辑删除
    @ApiOperation("逻辑删除接口")
    @PreAuthorize("hasAuthority('bnt.sysRole.remove')")
    @DeleteMapping("/remove/{id}")
    public Result removeRole(@PathVariable("id") Long id){
        boolean bool = sysRoleService.removeById(id);
        if(bool){
            return Result.ok();
        }else{
            return Result.fail();
        }
    }

    //查询所有
    @ApiOperation("查询所有角色接口")
    @PreAuthorize("hasAuthority('bnt.sysRole.list')")
    @GetMapping("/findAll")
    public Result findAllRole(){
        return Result.ok(sysRoleService.list());
    }

    //分页查询
    @ApiOperation("条件分页查询")
    @PreAuthorize("hasAuthority('bnt.sysRole.list')")
    @GetMapping("/{page}/{limit}")
    public Result  findPageQueryRole(@PathVariable("page") Long page, @PathVariable("limit") Long limit, SysRoleQueryVo sysRoleQueryVo){
        //创建apge对象
        System.out.println("page ==== " + page);
        System.out.println("limit ==== " + limit);
//        if(null == sysRoleQueryVo){
//            sysRoleQueryVo = new SysRoleQueryVo();
            sysRoleQueryVo.setRoleName("用户");
//        }
        System.out.println("========================" + sysRoleQueryVo.getRoleName() + "=========================");
        Page pageParam = new Page<>(page, limit);
        IPage pageModel = sysRoleService.mySelectPage(pageParam, sysRoleQueryVo);
        System.out.println(pageModel.getRecords());
        return Result.ok(pageModel);
    }
  1. 全局异常处理器配置
package com.xxz.system.exception;

import com.xxz.common.result.Result;
import com.xxz.common.result.ResultCodeEnum;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;


/**
 * 全局异常处理器-通过aop方式
 */
@ControllerAdvice
public class GlobalExcetionHandler {

    //1.全局异常
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result error(Exception e){
        return Result.fail().message("执行了全局异常处理");
    }

    //2.特定异常
    @ExceptionHandler(ArithmeticException.class)
    @ResponseBody
    public Result error(ArithmeticException e){
        return Result.fail().message("执行了特定异常处理");
    }


    //3.自定义异常
    @ExceptionHandler(GuiguException.class)
    @ResponseBody
    public Result error(GuiguException e){
        return Result.fail().message("执行了自定义异常处理");
    }

    /**
     * spring security的异常:[要导security包] import org.springframework.security.access.AccessDeniedException;
     * @param e
     * @return
     */
    //[new 权限]
    //4.无法访问异常
    @ExceptionHandler(AccessDeniedException.class)
    @ResponseBody
    public Result error(AccessDeniedException e){
        return Result.fail().code(ResultCodeEnum.PERMISSION.getCode()).message("没有当前功能操作权限");
    }

}

你可能感兴趣的:(security,springboot)