springboot2.0--结合spring security5.0进行权限控制,从数据库中取权限信息及增加验证码

1.在pom.xml中增加spring security jar的引用:     

  <!--引入spring security-->
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
   </dependency>
2.增加一个配置类MySecurityConfig,该类继承WebSecurityConfigurerAdapter;
package com.lxht.emos.config;

import com.lxht.emos.bean.MenuBean;
import com.lxht.emos.security.MyUserDetailsService;
import com.lxht.emos.security.MyValidCodeProcessingFilter;
import com.lxht.emos.service.MenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;

import java.util.List;
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    private String loginPage = "/userLogin";
    private String failureUrl = "/userLogin?error=T";
    private String successUrl = "/userLogin?error=F";
    private String applyCheckCode = "/applyCheckCode";

    @Autowired
    MenuService menuService;

    public MySecurityConfig() {
        super(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.securityContext().securityContextRepository(securityContextRepository());
        //从数据库读取对应的url地址及权限角色
        List<MenuBean>  menuBeanList =  menuService.getMenus();
        for(MenuBean tmpBean : menuBeanList) {
            http.authorizeRequests().antMatchers(tmpBean.getMenuUrl()).hasAnyRole(tmpBean.getRoleIds().split(","));
        }
        //设置登录界面
        http.authorizeRequests()
                .and()
             //增加一个对验证码进行判断的filte
             .addFilterBefore(validCodeProcessingFilter(),UsernamePasswordAuthenticationFilter.class) 
             .formLogin().loginPage(getLoginPage()).failureUrl(getFailureUrl())
                           .failureForwardUrl(getFailureUrl()).
                           successForwardUrl(getSuccessUrl()).permitAll();
    }
   
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
     //设置根据用户名获取用户信息的自定义userDetailsService类,该类从数据库中读用用户信息

auth.userDetailsService(userDetailsService());

    }
    
    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
        web.ignoring().antMatchers(applyCheckCode); //设置申请验证码的url不进行访问控制
    }

    @SuppressWarnings("deprecation")
    @Bean
    public static NoOpPasswordEncoder passwordEncoder() { 
      //这里使用了密码不进行加密验证,正式项目还是必须要用加密验证方式
      return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance(); 
}

@Bean 
public UserDetailsService userDetailsService() {
 MyUserDetailsService myUserDetailsService = new MyUserDetailsService(); return myUserDetailsService; 
 } 
@Bean 
public SecurityContextRepository securityContextRepository() {

      //设置对spring security的UserDetails进行session保存,这个必须要有,不然不会保存至session对应的缓存redis中
        HttpSessionSecurityContextRepository httpSessionSecurityContextRepository = 
            new HttpSessionSecurityContextRepository();
        return httpSessionSecurityContextRepository;
    }

    @Bean
    public  MyValidCodeProcessingFilter validCodeProcessingFilter() throws Exception{
        //设置对应的验证码验证filter,上面configure方法中通过
        //.addFilterBefore(validCodeProcessingFilter(),UsernamePasswordAuthenticationFilter.class)
        //将该filter放至UsernamePasswordAuthenticationFilter之间
        MyValidCodeProcessingFilter myValidCodeProcessingFilter = new MyValidCodeProcessingFilter(getLoginPage());
        myValidCodeProcessingFilter.getFailureHandler().setDefaultFailureUrl(getFailureUrl());
        myValidCodeProcessingFilter.setAuthenticationManager(this.authenticationManager());

        return myValidCodeProcessingFilter;
    }

    public String getLoginPage() {
        return loginPage;
    }

    public void setLoginPage(String loginPage) {
        this.loginPage = loginPage;
    }

    public String getFailureUrl() {
        return failureUrl;
    }

    public void setFailureUrl(String failureUrl) {
        this.failureUrl = failureUrl;
    }

    public String getSuccessUrl() {
        return successUrl;
    }

    public void setSuccessUrl(String successUrl) {
        this.successUrl = successUrl;
    }

    public String getApplyCheckCode() {
        return applyCheckCode;
    }

    public void setApplyCheckCode(String applyCheckCode) {
        this.applyCheckCode = applyCheckCode;
    }
}
下面对该类代码进行一下详细的解译:
(1)@EnableWebSecurity表示启用spring security进行web访问控制;
(2)引入MenuService menuService,表示从数据库读取配置的url访问控制;menuService从数据库(mysql)中读取的数据格式(表名:sys_menus)如下:
    
menu_id menu_name     menu_url rold_ids
1 用户登录 /userAuth 1,2
2 总控开关 /** 1,2

    其中role_ids表示访问该url项的角色id,多个角色以,分隔;
(3)构造方法,super(true)表示取消spring security的默认设置项,因为我的应用结合了cache redis和session redis,
   发现不取消默认设置项,自定义的userDetailsService不执行; 
   public MySecurityConfig() {
      super(true);
   }
(4)configure(HttpSecurity http)方法代码说明
   略,详见方法听注释
(5)对应的MyUserDetailsService类的内容:
     
package com.lxht.emos.security;

import com.lxht.emos.bean.UserBean;
import com.lxht.emos.bean.UserRolesBean;
import com.lxht.emos.bean.UserSessionBean;
import com.lxht.emos.service.UserService;
import com.lxht.emos.utils.ConstantVal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
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;
import org.springframework.util.AutoPopulatingList;

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

public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserService userService; //从数据库中读取用户的信息

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        ArrayList<SimpleGrantedAuthority> authorities = new ArrayList();
        UserBean userBean = new UserBean();
        userBean.setLoginName(s);
        UserBean userInfo = userService.getUserInfo(userBean); //读取用户信息
        if(userInfo == null) {
            throw new UsernameNotFoundException("username is not exist.");
        }

        UserRolesBean userRolesBean = new UserRolesBean();
        userRolesBean.setUserId(userInfo.getUserId());
        List<UserRolesBean> rolesBeanList =  userService.getRolesBeanById(userRolesBean); //根据用户id读取用户的权限角色
        for(UserRolesBean tmpRoleBean : rolesBeanList) {
            authorities.add(new SimpleGrantedAuthority(ConstantVal.ROLE_PREFIX + tmpRoleBean.getRoleId().toString()));
        }

        User userDetails = new User(userInfo.getLoginName(),userInfo.getUserPwd(),authorities);
        return userDetails;
    }
}   
 用户表及用户与角色对应表内容:
    
(6)自定义MyValidCodeProcessingFilter内容:
 
package com.lxht.emos.security;

import com.lxht.emos.utils.ConstantVal;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

public class MyValidCodeProcessingFilter extends AbstractAuthenticationProcessingFilter {
    public MyValidCodeProcessingFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
        setContinueChainBeforeSuccessfulAuthentication(true); //设置为true,否则该filter执行完毕后,将不会转至UsernamePasswordAuthenticationFilter执行,原因请查看AbstractAuthenticationProcessingFilter类的dofilter方法
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
        String username = request.getParameter(ConstantVal.PAR_USER_NAME);
        String password = request.getParameter(ConstantVal.PAR_PASSWORD);
        String validCode = request.getParameter(ConstantVal.PAR_CHECK_CODE);
        valid(validCode, request.getSession()); //验证用户从前台录入的验证码是否有效
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
        return token;
    }
    
    //进行验证码验证的方法
    public void valid(String validCode, HttpSession session) {
        if (validCode == null) {
            throw new BadCredentialsException("验证码为空!");
        }

        String checkCode = (String)session.getAttribute(ConstantVal.CHECK_CODE);
        if (!validCode.equals(checkCode)) {
            session.removeAttribute(ConstantVal.CHECK_CODE);
            throw new BadCredentialsException("验证码错误!");
        } else {
            session.removeAttribute(ConstantVal.CHECK_CODE);
        }
    }
    
    public SimpleUrlAuthenticationFailureHandler getFailureHandler() {
        SimpleUrlAuthenticationFailureHandler failureHandler = (SimpleUrlAuthenticationFailureHandler)super.getFailureHandler();
        
     //本句也必须设置为true,否则验证失败后,不会转至MySecurityConfig类中配置的failureUrl方法中
     failureHandler.setUseForward(true);
     return failureHandler;

     }

}

(7)Controller层的类 AuthValidController

package com.lxht.emos.controller;
import com.lxht.emos.bean.ReturnBean;
import com.lxht.emos.bean.UserBean;
import com.lxht.emos.bean.UserRolesBean;
import com.lxht.emos.bean.UserSessionBean;
import com.lxht.emos.event.UserModifiedPublisher;
import com.lxht.emos.exception.AuthException;
import com.lxht.emos.service.MenuService;
import com.lxht.emos.service.UserService;
import com.lxht.emos.utils.ConstantVal;
import com.sun.org.apache.bcel.internal.generic.RETURN;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.WebAttributes;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;

@RestController
public class AuthValidController {
    @Autowired
    private UserService userService;

    @RequestMapping("/")
    public UserBean authCenterHome() {
        User userSessionBean = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        UserBean userBean = new UserBean();
        userBean.setLoginName(userSessionBean.getUsername());
        UserBean userInfo = userService.getUserInfo(userBean);
        return userInfo;
    }

    @RequestMapping("/userLogin")
    public UserBean userLogin(HttpServletRequest request) throws Exception{

        UserBean userInfo = new UserBean();
        String username = "";

        String lb = request.getParameter("error");

        if("T".equals(lb)) {
            Exception exception = (Exception)request.getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
            if(exception != null) {
                throw new AuthException(exception.getMessage());
            } else {
                String errMsg = "用户名或密码错误!";
                throw new AuthException(errMsg);
            }
        } else {
            User userSessionBean = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            UserBean userBean = new UserBean();
            userBean.setLoginName(userSessionBean.getUsername());
        }

        userInfo.setLoginName(username);
        return userInfo;
    }

    @RequestMapping("/userAuth")
    public UserBean userAuth() {
        User userSessionBean = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        UserBean userBean = new UserBean();
        userBean.setLoginName(userSessionBean.getUsername());
        UserBean userInfo = userService.getUserInfo(userBean);
        return userInfo;
    }

    @RequestMapping("/applyCheckCode") 
    public ReturnBean applyCheckCode(HttpServletRequest request) {
        ReturnBean returnBean = new ReturnBean();
        request.getSession().setAttribute("CHECK_CODE","1111"); //这里只是做demo演示,没有使用插件来生成正成的图片验证码,只是简单的固定将验证码1111写入了session中
        returnBean.setRetCode("1");
        return returnBean;
    }
}

(8)通过以上的代码和配置,即可以完成spring security的自定义权限控制和增加验证码功能;
(9)测试分两步:
     第一步:http://localhost:8000/applyCheckCode 获取验证码;     
    第二步:访问/userLogin进行用户登录操作
springboot2.0--结合spring security5.0进行权限控制,从数据库中取权限信息及增加验证码

 

(10)注意,我这里使用到了cache 和session均存至redis的配置,具体的配置说明方法,请参考另一篇文章:
      springboot2.0-启动cache和session同时存入redis(使用不同的数据库)
      https://blog.csdn.net/fycghy0803/article/details/80482447

(11) 系统demo源码地址:
      https://github.com/fycghy0803/authCenter/tree/master