SpringBoot 整合 spring-security 笔记

1,162 阅读4分钟

文档相关:

spring.io/projects/sp…
www.thymeleaf.org/doc/article…

1.SpringBoot + Security

1.add Depending

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2.Config Code

package com.ml.jkeep.internal.auth;

import com.alibaba.fastjson.JSONObject;
import com.ml.jkeep.common.constant.Common;
import com.ml.jkeep.common.constant.ResultMsg;
import com.ml.jkeep.common.vo.RestVo;
import com.ml.jkeep.service.system.impl.AuthServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.ObjectPostProcessor;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;

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

/**
 * 安全认证配置
 *
 * @author 谭良忠
 * @date 2019/6/20 10:27
 */
@Slf4j
@EnableWebSecurity
public class JKeepSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthServiceImpl userAuthService;
    @Autowired
    private JKeepSecurityMetadataSource securityMetadataSource;
    @Autowired
    private JKeepAccessDecisionManager accessDecisionManager;
    @Autowired
    private JKeepAccessDeniedHandler accessDeniedHandler;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userAuthService)    // 身份认证
                .passwordEncoder(new BCryptPasswordEncoder()); // 加密方式
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setSecurityMetadataSource(securityMetadataSource); // 过滤器调用安全元数据源
                        o.setAccessDecisionManager(accessDecisionManager);    // 鉴权策略
                        return o;
                    }
                })
                .and()
                .formLogin()
                .loginPage(Common.LOGIN_PAGE_URL)
                .loginProcessingUrl("/login")
                .usernameParameter("username").passwordParameter("password")
                .failureHandler((req, resp, e) -> {
                    log.error("登陆失败处理 : {}", JSONObject.toJSONString(req.getParameterMap()));
                    ResultMsg resultMsg;
                    if (e instanceof BadCredentialsException ||
                            e instanceof UsernameNotFoundException) {
                        // 账户名或者密码输入错误
                        resultMsg = ResultMsg.LOGIN_FAIL_WRONG_PASSWORD;
                    } else if (e instanceof LockedException) {
                        // 账户被锁定,请联系管理员!
                        resultMsg = ResultMsg.LOGIN_FAIL_LOCKED;
                    } else if (e instanceof CredentialsExpiredException) {
                        // 登录过期,请重新登录!
                        resultMsg = ResultMsg.LOGIN_FAIL_CREDENTIALS_EXPIRED;
                    } else if (e instanceof AccountExpiredException) {
                        // 账户过期,请联系管理员!
                        resultMsg = ResultMsg.LOGIN_FAIL_ACCOUNT_EXPIRED;
                    } else if (e instanceof DisabledException) {
                        // 账户被禁用,请联系管理员!
                        resultMsg = ResultMsg.LOGIN_FAIL_DISABLED;
                    } else {
                        // 登录失败,请联系管理员!
                        resultMsg = ResultMsg.LOGIN_FAIL;
                    }
                    if (req.getHeader(Common.CSRF_TOKEN_KEY) != null) {
                        // Ajax 提交
                        resp.setStatus(HttpServletResponse.SC_OK);
                        resp.setContentType("application/json;charset=UTF-8");
                        PrintWriter out = resp.getWriter();
                        out.write(JSONObject.toJSONString(RestVo.FAIL(resultMsg)));
                        out.flush();
                        out.close();
                    } else {
                        // 表单提交
                        resp.sendRedirect("/login.html");
                    }
                })
                .successHandler((req, resp, auth) -> {
                    if (req.getHeader(Common.CSRF_TOKEN_KEY) != null) {
                        // Ajax 提交
                        resp.setStatus(HttpServletResponse.SC_OK);
                        resp.setContentType("application/json;charset=UTF-8");
                        PrintWriter out = resp.getWriter();
                        out.write(JSONObject.toJSONString(RestVo.SUCCESS("登录成功")));
                        out.flush();
                        out.close();
                    } else {
                        // 表单提交
                        resp.sendRedirect("/index");
                    }
                })
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler((req, resp, authentication) -> {
                    //  注销成功重定向登录页
                    if (req.getHeader(Common.CSRF_TOKEN_KEY) != null) {
                        // Ajax 提交
                        resp.setStatus(HttpServletResponse.SC_OK);
                        resp.setContentType("application/json;charset=UTF-8");
                        PrintWriter out = resp.getWriter();
                        out.write(JSONObject.toJSONString(RestVo.SUCCESS("登出成功")));
                        out.flush();
                        out.close();
                    } else {
                        // 表单提交
                        resp.sendRedirect("/login.html");
                    }
                })
                .permitAll()
                .and()
                .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler) // 访问被拒策略
                // .and().csrf().disable() // 默认启用csrf, 若不需要可以禁用
            ;
    }

    @Override
    public void configure(WebSecurity web) {
        // 忽略请求, 无需鉴权即可访问
        web.ignoring().antMatchers("/plugins/**", "**.js", "/img/**", "**.css", "/favicon.ico", "/401.html", "/404.html");
    }

}

package com.ml.jkeep.internal.auth;

import com.alibaba.fastjson.JSON;
import com.ml.jkeep.common.constant.Common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

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

/**
 * 鉴权策略
 *
 * @author 谭良忠
 * @date 2019/6/20 13:24
 */
@Slf4j
@Component
public class JKeepAccessDecisionManager implements AccessDecisionManager {

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        FilterInvocation filterInvocation = (FilterInvocation) object;
        String requestUrl = filterInvocation.getRequestUrl();
        log.info("requestUrl : {} , configAttributes: {} , authentication: {}", requestUrl, JSON.toJSONString(configAttributes), JSON.toJSONString(authentication));
        if (!antPathMatcher.match(Common.LOGIN_PAGE_URL, requestUrl)) {
            if (authentication instanceof AnonymousAuthenticationToken) {
                throw new BadCredentialsException("未登录");
            }
        } else {
            // 登陆页面无需鉴权可访问
            return;
        }
        // 验证用户是否有权访问当前地址
        // configAttributes 可以访问 url 的角色
        // authentication.getAuthorities() 用户所有角色
        List<GrantedAuthority> isPermission = new ArrayList<>();
        configAttributes.forEach(configAttribute -> isPermission.addAll(authentication.getAuthorities().stream().filter(authority -> authority.getAuthority().equals(configAttribute.getAttribute())).collect(Collectors.toList())));
        if (isPermission.isEmpty()) {
            log.info("用户权限不足, 无法访问!");
            throw new AccessDeniedException("用户权限不足, 无法访问!");
        }
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return false;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return false;
    }
}

package com.ml.jkeep.internal.auth;

import com.alibaba.fastjson.JSON;
import com.ml.jkeep.common.constant.Common;
import com.ml.jkeep.jpa.system.vo.HrefPermissionVo;
import com.ml.jkeep.service.system.impl.HrefPermissionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

/**
 * 过滤器调用安全元数据源
 *
 * @author 谭良忠
 * @date 2019/6/20 11:48
 */
@Slf4j
@Component
public class JKeepSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Autowired
    private HrefPermissionService hrefPermissionService;

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        FilterInvocation filterInvocation = (FilterInvocation) object;
        String requestUrl = filterInvocation.getRequestUrl();
        Set<HrefPermissionVo> allPer = hrefPermissionService.hrefPermission();
        Set<String> roles = new HashSet<>();
        for (HrefPermissionVo per : allPer) {
            if (antPathMatcher.match(per.getHref(), requestUrl)) {
                roles.add(per.getCode());
            }
        }
        if (!CollectionUtils.isEmpty(roles)) {
            log.info("JKeepSecurityMetadataSource, requestUrl : {}, roles: {}", requestUrl, JSON.toJSONString(roles));
            return SecurityConfig.createList(roles.toArray(new String[0]));
        }
        log.info("JKeepSecurityMetadataSource, requestUrl : {}, roles: {}", requestUrl, Common.ROLE_DEFAULT);
        // 当 url 没有配置权限控制时, 赋予默认角色可访问.即所有用户都可访问
        return SecurityConfig.createList(Common.ROLE_DEFAULT);
    }


    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return false;
    }
}

package com.ml.jkeep.internal.auth;

import com.alibaba.fastjson.JSONObject;
import com.ml.jkeep.common.constant.Common;
import com.ml.jkeep.common.constant.ResultMsg;
import com.ml.jkeep.common.vo.RestVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

/**
 * 访问被拒处理
 *
 * @author 谭良忠
 * @date 2019/6/20 11:41
 */
@Slf4j
@Component
public class JKeepAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        log.info("处理访问被拒 , Parameter: {}, RequestURI: {}, RemoteUser: {}, RemoteAddr: {}, Message: {}",
                JSONObject.toJSONString(request.getParameterMap()), request.getRequestURI(),
                request.getRemoteUser(), request.getRemoteAddr(), accessDeniedException.getMessage());
        if (request.getHeader(Common.CSRF_TOKEN_KEY) != null) {
            // Ajax 提交
            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType("application/json;charset=UTF-8");
            PrintWriter out = response.getWriter();
            out.write(JSONObject.toJSONString(RestVo.FAIL(ResultMsg.ACCESS_DENIED)));
            out.flush();
            out.close();
        } else {
            // 表单提交 . 重定向401页
            response.sendRedirect("/401.html");
        }
    }
}

package com.ml.jkeep.jpa.system.entity.sys;

import com.ml.jkeep.common.constant.Common;
import com.ml.jkeep.jpa.system.vo.HrefPermissionVo;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.*;

/**
 * 用户身份认证信息 - Entity
 *
 * @author 谭良忠
 * @date 2019/6/20 11:00
 */
@Data
public class UserAuth implements UserDetails {

    private String username;
    private String password;
    private Set<HrefPermissionVo> hrefPer = new HashSet<>();

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        hrefPer.forEach(per -> authorities.add(new SimpleGrantedAuthority(per.getCode())));
        // 默认角色
        authorities.add(new SimpleGrantedAuthority(Common.ROLE_DEFAULT));
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        // TODO 帐户是否过期
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        // TODO 帐户是否被冻结
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        // TODO 帐户密码是否过期
        return true;
    }

    @Override
    public boolean isEnabled() {
        // TODO 帐号是否可用
        return true;
    }
}

package com.ml.jkeep.service.system.impl;

import com.ml.jkeep.jpa.system.entity.sys.User;
import com.ml.jkeep.jpa.system.entity.sys.UserAuth;
import com.ml.jkeep.jpa.system.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
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.Service;

/**
 * 鉴权 -impl
 *
 * @author 谭良忠
 * @date 2019/6/20 10:58
 */
@Slf4j
@Service
public class AuthServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private HrefPermissionService hrefPermissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("loadUserByUsername, username: {}", username);
        User user = userRepository.findByUsername(username);
        if (user == null) {
            log.error("用户名不存在, {}", username);
            throw new UsernameNotFoundException("请输入正确的用户名");
        }
        UserAuth userAuth = new UserAuth();
        userAuth.setUsername(user.getUsername());
        userAuth.setPassword(user.getPassword());
        userAuth.setHrefPer(hrefPermissionService.hrefPermission(user.getUserId()));
        return userAuth;
    }

}

获取当前登录的用户信息

package com.ml.jkeep.internal.auth;

import com.alibaba.fastjson.JSONObject;
import com.ml.jkeep.jpa.system.entity.sys.UserAuth;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.ArrayList;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 令牌持有者(身份认证相关信息)
 *
 * @author 谭良忠
 * @date 2019/7/17 15:10
 */
public class JKeepSecurityContextHolder {

    /**
     * 获取 sessionId
     *
     * @return sessionId
     */
    public static String getSessionId() {
        return getDetails().getString("sessionId");
    }

    /**
     * 存储有关身份验证请求的其他详细信息 IP地址 , sessionId 等
     *
     * @return {"remoteAddress":"0:0:0:0:0:0:0:1","sessionId":"DECEC3BE2FC3F2D2002624E155939F35"}
     */
    public static JSONObject getDetails() {
        return (JSONObject) SecurityContextHolder.getContext().getAuthentication().getDetails();
    }

    /**
     * 获得用户信息
     *
     * @return 用户信息
     */
    public static UserAuth getUserInfo() {
        return (UserAuth) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }

    /**
     * 是否已经登陆
     *
     * @return 是否已经登陆
     */
    public static boolean isAuthenticated() {
        return SecurityContextHolder.getContext().getAuthentication().isAuthenticated();
    }

    /**
     * 获得用户所持有的角色
     *
     * @return 角色集合
     */
    public static Set<String> getRoles() {
        return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication().getAuthorities()).orElse(new ArrayList<>()).stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
    }
}

2. SpringBoot + Thymeleaf + Spring Security

1.add Depending

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

2.示例

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="utf-8">
    <title>Demo</title>
</head>
<body>
    登录名: <span sec:authentication="name"></span><br/>
    用户所有权限: <span sec:authentication="principal.authorities"></span>
     <div sec:authorize="${hasRole('DEFAULT')}">
         是否是该角色
     </div>
     <div sec:authorize="${hasAuthority('DEMO')}">
         是否有权限
     </div>
</body>