spring-boot 整合 spring-security(用户管理,角色管理,权限管理,验证码,注册功能实现)

**本人第一次写博客,代码如有错误之处,烦请指出,十分感谢!话不多说,进入正题:**

1.开发环境:spring-boot + mybatis + mysql,开发工具:idea。
2.首先想要整合spring-security做登录管理就要明白我们做什么,用户,角色,权限三者之间的关系,参考以下文章做了解:扩展RBAC用户角色权限设计方案
文章中代码参考以下博客做的整合:
(1)【详细】Spring Boot框架整合Spring Security实现安全访问控制
(2) SpringBoot集成Spring Security(1)——入门程序
(3) Spring Security之Config模块详解(TODO)
(4) Spring Boot 整合 Security 权限控制 - 第006章 - 自定义failureHandler
3.数据库表建立

  • 用户表(sys_user)
CREATE TABLE `sys_user` (                              
           `id` int(11) NOT NULL AUTO_INCREMENT,                
           `name` varchar(255) NOT NULL,                        
           `password` varchar(255) NOT NULL,                    
           PRIMARY KEY (`id`)                                   
         ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8  `
  • 角色表(sys_role)
CREATE TABLE `sys_role` (             
            `id` int(11) NOT NULL,              
            `name` varchar(255) NOT NULL,       
            PRIMARY KEY (`id`)                  
          ) ENGINE=InnoDB DEFAULT CHARSET=utf8 
  • 用户角色中间表(sys_user_role)
CREATE TABLE `sys_user_role` (                                                                                       
                 `user_id` int(11) NOT NULL,                                                                                        
                 `role_id` int(11) NOT NULL,                                                                                        
                 PRIMARY KEY (`user_id`,`role_id`),                                                                                 
                 KEY `fk_role_id` (`role_id`),                                                                                      
                 CONSTRAINT `fk_role_id` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,  
                 CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE   
               ) ENGINE=InnoDB DEFAULT CHARSET=utf8     
  • 权限表(sys_resource)
CREATE TABLE `sys_resource` (         
               `id` int(11) NOT NULL,              
               `url` varchar(255) NOT NULL,        
               `res_name` varchar(255) NOT NULL,   
               PRIMARY KEY (`id`)                  
             ) ENGINE=InnoDB DEFAULT CHARSET=utf8  
  • 角色权限中间表(sys_resource_role)
CREATE TABLE `sys_resource_role` (                                                                                     
                    `res_id` int(11) NOT NULL,                                                                                           
                    `role_id` int(11) NOT NULL,                                                                                          
                    PRIMARY KEY (`res_id`,`role_id`),                                                                                    
                    KEY `fk_role_id` (`role_id`),                                                                                        
                    CONSTRAINT `fk_res_id` FOREIGN KEY (`res_id`) REFERENCES `sys_resource` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,  
                    CONSTRAINT `fk_role_id1` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE    
                  ) ENGINE=InnoDB DEFAULT CHARSET=utf8        

4.pom引入依赖

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

5.dao层代码我用的mybatis自带的generator自动生成的,此处省略。
6.service层代码

  • SysUserService + SysUserServiceImpl
package com.example.sptingboot_mybatis.service;

import com.example.sptingboot_mybatis.demo.SysUser;

import java.util.List;

public interface SysUserService {
   SysUser findUserById(Integer id);

   List<SysUser> findUserByName(String username);

   void insert(SysUser user);
}

------------------------------------------------------------------------------

package com.example.sptingboot_mybatis.service.impl;

import com.example.sptingboot_mybatis.dao.SysUserMapper;
import com.example.sptingboot_mybatis.demo.SysUser;
import com.example.sptingboot_mybatis.demo.SysUserExample;
import com.example.sptingboot_mybatis.service.SysUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class SysUserServiceImpl implements SysUserService {
   private static final Logger logger = LoggerFactory.getLogger(SysUserServiceImpl.class);
   @Autowired
   SysUserMapper sysUserMapper;
   @Override
   public SysUser findUserById(Integer id){
       SysUser sysUser = sysUserMapper.selectByPrimaryKey(id);
       logger.info("根据id查询到的user:"+sysUser.toString());
       return sysUser;
   }
   @Override
   public List<SysUser> findUserByName(String username){
       SysUserExample sysUserExample = new SysUserExample();
       SysUserExample.Criteria criteria = sysUserExample.createCriteria();
       criteria.andNameEqualTo(username);
       List<SysUser> sysUsers = sysUserMapper.selectByExample(sysUserExample);
       logger.info("根据名字查询到的user:"+sysUsers);
       return sysUsers;
   }

   @Override
   public void insert(SysUser user) {
       sysUserMapper.insertSelective(user);
   }
}
  • SysRoleService + SysRoleServiceImpl
package com.example.sptingboot_mybatis.service;

import com.example.sptingboot_mybatis.demo.SysRole;

public interface SysRoleService {
    public SysRole findRoleById(Integer id);
    public SysRole findRoleByName(String name);
}

------------------------------------------------------------------------------

package com.example.sptingboot_mybatis.service.impl;

import com.example.sptingboot_mybatis.dao.SysRoleMapper;
import com.example.sptingboot_mybatis.demo.SysRole;
import com.example.sptingboot_mybatis.demo.SysRoleExample;
import com.example.sptingboot_mybatis.service.SysRoleService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class SysRoleServiceImpl implements SysRoleService {
    private static final Logger logger = LoggerFactory.getLogger(SysRoleServiceImpl.class);
    @Autowired
    SysRoleMapper sysRoleMapper;

    @Override
    public SysRole findRoleById(Integer id) {
        SysRole sysRole = sysRoleMapper.selectByPrimaryKey(id);
        logger.info("根据id查询到的role:" + sysRole.toString());
        return sysRole;
    }

    @Override
    public SysRole findRoleByName(String name) {
        SysRoleExample sysRoleExample = new SysRoleExample();
        sysRoleExample.createCriteria().andNameEqualTo(name);
        List<SysRole> sysRoles = sysRoleMapper.selectByExample(sysRoleExample);
        if(sysRoles == null || sysRoles.size()==0){
            return null;
        }
        return sysRoles.get(0);
    }

}
  • SysUserRoleService + SysUserRoleServiceImpl
package com.example.sptingboot_mybatis.service;

import com.example.sptingboot_mybatis.demo.SysUserRoleKey;

import java.util.List;

public interface SysUserRoleService {
    List<SysUserRoleKey> findRoleListByUserId(Integer userId);

    void insert(int id, Integer roleId);
}

------------------------------------------------------------------------------

package com.example.sptingboot_mybatis.service.impl;

import com.example.sptingboot_mybatis.dao.SysUserRoleMapper;
import com.example.sptingboot_mybatis.demo.SysUserRoleExample;
import com.example.sptingboot_mybatis.demo.SysUserRoleKey;
import com.example.sptingboot_mybatis.service.SysUserRoleService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class SysUserRoleServiceImpl implements SysUserRoleService {
    private static final Logger logger = LoggerFactory.getLogger(SysUserRoleServiceImpl.class);
    @Autowired
    SysUserRoleMapper sysUserRoleMapper;
    @Override
    public List<SysUserRoleKey> findRoleListByUserId(Integer userId){
        SysUserRoleExample sysUserRoleExample = new SysUserRoleExample();
        sysUserRoleExample.createCriteria().andUserIdEqualTo(userId);
        List<SysUserRoleKey> sysUserRoleKeys = sysUserRoleMapper.selectByExample(sysUserRoleExample);
        logger.info("根据userId查询到的Roles"+sysUserRoleKeys.toString());
        return sysUserRoleKeys;
    }

    @Override
    public void insert(int id, Integer roleId) {
        SysUserRoleKey sysUserRoleKey = new SysUserRoleKey();
        sysUserRoleKey.setRoleId(roleId);
        sysUserRoleKey.setUserId(id);
        sysUserRoleMapper.insertSelective(sysUserRoleKey);

    }
}
  • SysResourceService + SysResourceServiceImpl
package com.example.sptingboot_mybatis.service;
import com.example.sptingboot_mybatis.demo.SysResource;

public interface SysResourceService {
    public SysResource findResourceByUrl(String url);
}

------------------------------------------------------------------------------

package com.example.sptingboot_mybatis.service.impl;

import com.example.sptingboot_mybatis.dao.SysResourceMapper;
import com.example.sptingboot_mybatis.demo.SysResource;
import com.example.sptingboot_mybatis.demo.SysResourceExample;
import com.example.sptingboot_mybatis.service.SysResourceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class SysResourceServiceImpl implements SysResourceService {
    @Autowired
    SysResourceMapper sysResourceMapper;
    @Override
    public SysResource findResourceByUrl(String url) {
        SysResourceExample sysResourceExample = new SysResourceExample();
        sysResourceExample.createCriteria().andUrlEqualTo(url);
        List<SysResource> sysResources = sysResourceMapper.selectByExample(sysResourceExample);
        if(sysResources == null || sysResources.size()==0){
            return null;
        }
        return sysResources.get(0);
    }
}
  • SysResourceRoleService + SysResourceRoleServiceImpl
package com.example.sptingboot_mybatis.service;

import com.example.sptingboot_mybatis.demo.SysResourceRoleKey;

import java.util.List;

public interface SysResourceRoleService {
    public List<SysResourceRoleKey> fingSysResourceRoleKeyByResId(Integer resId);
}

------------------------------------------------------------------------------
package com.example.sptingboot_mybatis.service.impl;

import com.example.sptingboot_mybatis.dao.SysResourceRoleMapper;
import com.example.sptingboot_mybatis.demo.SysResourceRoleExample;
import com.example.sptingboot_mybatis.demo.SysResourceRoleKey;
import com.example.sptingboot_mybatis.service.SysResourceRoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class SysResourceRoleServiceImpl implements SysResourceRoleService {

    @Autowired
    SysResourceRoleMapper sysResourceRoleMapper;
    @Override
    public List<SysResourceRoleKey> fingSysResourceRoleKeyByResId(Integer resId) {
        SysResourceRoleExample sysResourceRoleExample = new SysResourceRoleExample();
        sysResourceRoleExample.createCriteria().andResIdEqualTo(resId);
        List<SysResourceRoleKey> sysResourceRoleKeys = sysResourceRoleMapper.selectByExample(sysResourceRoleExample);
        return sysResourceRoleKeys;
    }
}

7.controller层代码

package com.example.sptingboot_mybatis.controller;

import com.example.sptingboot_mybatis.demo.Msg;
import com.example.sptingboot_mybatis.demo.SysRole;
import com.example.sptingboot_mybatis.demo.SysUser;
import com.example.sptingboot_mybatis.demo.SysUserRoleKey;
import com.example.sptingboot_mybatis.service.SysRoleService;
import com.example.sptingboot_mybatis.service.SysUserRoleService;
import com.example.sptingboot_mybatis.service.SysUserService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

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


@Controller
public class LoginController {
   private Logger logger = LoggerFactory.getLogger(LoginController.class);
   @Autowired
   SysUserService sysUserService;
   @Autowired
   SysRoleService sysRoleService;
   @Autowired
   SysUserRoleService sysUserRoleService;

   @RequestMapping("/")
   public String showHome() {
       String name = SecurityContextHolder.getContext().getAuthentication().getName();
       logger.info("当前登陆用户:" + name);

       return "home";
   }

   @RequestMapping("/login")
   public String showLogin() {
       return "login";
   }

   @RequestMapping("/admin")
   @ResponseBody
   /* @PreAuthorize("hasPermission('/admin','r')")*/
   public String printAdmin() {
       return "如果你看见这句话,说明你可以访问/admin路径";
   }

   @RequestMapping("/user")
   @ResponseBody
   /*@PreAuthorize("hasPermission('/user','c')")*/
   public String printUser() {
       return "如果你看见这句话,说明你可以访问/user路径";
   }

   @RequestMapping("/all")
   @ResponseBody
   /*@PreAuthorize("hasPermission('/user','c')")*/
   public String printAll() {
       return "如果你看见这句话,说明你可以访问/all路径";
   }

   @RequestMapping(value = "/register", method = RequestMethod.GET)
   public String showRegister() {
       return "register";
   }

   @RequestMapping(value = "/register", method = RequestMethod.POST)
   public Object register(SysUser user, Integer[] roles) throws Exception {
       String name = user.getName();

       if (StringUtils.isBlank(name) || StringUtils.isBlank(user.getPassword())) {
           throw new Exception("输入数据错误");
       }

       if (sysUserService.findUserByName(name) != null && sysUserService.findUserByName(name).size()>0 ) {
           throw new Exception("用户名已被注册");
       }

       user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
       sysUserService.insert(user);
       int id = sysUserService.findUserByName(name).get(0).getId();
       for (Integer roleId : roles) {
           sysUserRoleService.insert(id, roleId);
       }
       return "redirect:/login";
   }

   @RequestMapping("/login/error")
   public String loginError(HttpServletRequest request) {
       AuthenticationException exception = (AuthenticationException) request.getSession().getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
       Msg msg = new Msg<>(false, exception.getMessage(), exception.toString());
       request.getSession().removeAttribute("msg");
       request.getSession().setAttribute("msg", msg);
       return "login";
   }
}

其中import com.example.sptingboot_mybatis.demo.Msg;是我自己用于返回信息的封装类,代码如下:(之所以包路径中命名为demo,是自己写错了,应该为pojo,后边也懒得改了,大家担待下)

package com.example.sptingboot_mybatis.demo;

import java.io.Serializable;

public class Msg<T> implements Serializable {

   private Boolean status;

   private String info;

   private T data;

   public Msg() {

   }

   public Msg(Boolean status, String info, T data) {
       this.status = status;
       this.info = info;
       this.data = data;
   }

   public Boolean getStatus() {
       return status;
   }

   public void setStatus(Boolean status) {
       this.status = status;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   public T getData() {
       return data;
   }

   public void setData(T data) {
       this.data = data;
   }
}

8.配置CustomUserDetailsService实现UserDetailsService用于基础的用户验证

package com.example.sptingboot_mybatis.security;

import com.example.sptingboot_mybatis.demo.SysRole;
import com.example.sptingboot_mybatis.demo.SysUser;
import com.example.sptingboot_mybatis.demo.SysUserRoleKey;
import com.example.sptingboot_mybatis.service.SysRoleService;
import com.example.sptingboot_mybatis.service.SysUserRoleService;
import com.example.sptingboot_mybatis.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Service
public class CustomUserDetailsService implements UserDetailsService {
   @Autowired
   SysRoleService sysRoleService;
   @Autowired
   SysUserService sysUserService;
   @Autowired
   SysUserRoleService sysUserRoleService;

   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
       Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
       List<SysUser> users = sysUserService.findUserByName(username);
       if(users == null && users.size()<1){
           throw new UsernameNotFoundException("用户名不存在");
       }
       SysUser sysUser = users.get(0);
       List<SysUserRoleKey> roleListByUserIds = sysUserRoleService.findRoleListByUserId(sysUser.getId());
       for (SysUserRoleKey roleListByUserId : roleListByUserIds) {
           SysRole sysRole = sysRoleService.findRoleById(roleListByUserId.getRoleId());
            grantedAuthorities.add( new SimpleGrantedAuthority(sysRole.getName()));
       }
       return new User(sysUser.getName(),sysUser.getPassword(),grantedAuthorities);
   }
}

9.接下来就是重点了,配置security的配置类WebSecurityConfig,但是在这之前我们按照思路要配置几个辅助模块。

  • 最开始我们想到的是登录成功后我跳转到那个页面,所以我们先配置登录成功的Handler:
package com.example.sptingboot_mybatis.security;

import com.example.sptingboot_mybatis.demo.Msg;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

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

@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    private ObjectMapper objectMapper;
    private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        logger.info("登录成功");
        this.returnSuccessPage(httpServletRequest,authentication,httpServletResponse);
    }
	//方法用于登录成功跳转页面,我这边多写了个根据security封装好的authentication获取登录成功用户所拥有的角色role,放到session中,这样在前端页面中根据从session中获得的不同的角色来展示不同的内容。
    private void returnSuccessPage(HttpServletRequest request,Authentication authentication,HttpServletResponse httpServletResponse) throws IOException, ServletException {
        String strUrl = request.getContextPath() + "/";
        StringBuffer stringBuffer = new StringBuffer("");
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        for (GrantedAuthority authority : authorities) {
            String role = authority.getAuthority();
            stringBuffer.append(","+role);
        }
        request.getSession().removeAttribute("msg");
        request.getSession().setAttribute("msg", new Msg<>(true, "登录成功",stringBuffer));
        httpServletResponse.sendRedirect(strUrl);
    }
}
  • 登录失败的Handler,登录失败后跳转回到登录界面
package com.example.sptingboot_mybatis.security;

import com.example.sptingboot_mybatis.demo.Msg;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

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

@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
    private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);
    @Autowired
    ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        logger.info("登录失败!");
        this.returnErrorPage(httpServletRequest, httpServletResponse, e);
    }
	//登录失败,跳转到login登录页面
    private void returnErrorPage(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String strUrl = request.getContextPath() + "/login";
        request.getSession().removeAttribute("msg");
        request.getSession().setAttribute("msg", new Msg<>(false, "用户名或者密码错误", exception.toString()));
        logger.info("登录失败,页面跳转:" + strUrl);
        response.sendRedirect(strUrl);
    }
}
  • 接下来我们要考虑到的是用户在登录成功以后有哪些URL可以访问,哪些不可以,即用户的权限问题,结合文章开头的resource权限表的建立(内含字段表示URL),我们可以这样想:假设我们通过程序监测到用户每一次的访问路径,比如:/login、/register 等等,再把我们监测到的URL和我们resource表内的URL做对比,如果登录用户所拥有的角色包含在我们数据库规定的能够访问此URL的角色中,则放行。
    • 拦截并获取用户请求的URL()
package com.example.sptingboot_mybatis.security;

import com.example.sptingboot_mybatis.demo.SysResource;
import com.example.sptingboot_mybatis.demo.SysResourceRoleKey;
import com.example.sptingboot_mybatis.demo.SysRole;
import com.example.sptingboot_mybatis.service.SysResourceRoleService;
import com.example.sptingboot_mybatis.service.SysResourceService;
import com.example.sptingboot_mybatis.service.SysRoleService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;

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

@Component
//接收用户请求的地址,返回访问该地址需要的所有权限
public class FilterInvocationSecurityMetadataSourceImpl implements FilterInvocationSecurityMetadataSource {
    private static final Logger logger = LoggerFactory.getLogger(FilterInvocationSecurityMetadataSourceImpl.class);
    @Autowired
    SysResourceService sysResourceService;
    @Autowired
    SysResourceRoleService sysResourceRoleService;
    @Autowired
    SysRoleService sysRoleService;


    @Override
    //接收用户请求的地址,返回访问该地址需要的所有权限
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        //得到用户的请求地址,控制台输出一下
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        logger.info("得到用户的请求地址:" + requestUrl);
        //如果登录,注册,验证码刷新路径就不需要权限
        if ("/login".equals(requestUrl)  || requestUrl.contains("/getVerifyCode") || "/register".equals(requestUrl) || "/login/error".equals(requestUrl)   ) {
            return null;
        }
        SysResource sysResource = sysResourceService.findResourceByUrl(requestUrl);
        //如果没有匹配的url则说明大家都可以访问
        if (sysResource == null) {
            return SecurityConfig.createList("ROLE_LOGIN");
        }
        List<SysResourceRoleKey> sysResourceRoleKeys = sysResourceRoleService.fingSysResourceRoleKeyByResId(sysResource.getId());
        if(sysResourceRoleKeys == null || sysResourceRoleKeys.size() == 0){
            throw new BadCredentialsException("访问路径未分配角色");
        }
        List<String> roleNameList = new ArrayList<>();
        for (SysResourceRoleKey sysResourceRoleKey : sysResourceRoleKeys) {
            Integer roleId = sysResourceRoleKey.getRoleId();
            SysRole sysRole = sysRoleService.findRoleById(roleId);
            String roleName = sysRole.getName();
            roleNameList.add(roleName);
        }
        return SecurityConfig.createList(roleNameList.toArray(new String[sysResourceRoleKeys.size()]));
    }

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

    @Override
    public boolean supports(Class<?> clazz) {
        return false;
    }
}
  • 确认用户拥有的角色是否包含在能够访问拦截到的URL的角色中
package com.example.sptingboot_mybatis.security;

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.stereotype.Component;

import java.util.Collection;

@Component
//Security需要用到一个实现了AccessDecisionManager接口的类
//类功能:根据当前用户的信息,和目标url涉及到的权限,判断用户是否可以访问
//判断规则:用户只要匹配到目标url权限中的一个role就可以访问
public class AccessDecisionManagerImpl implements AccessDecisionManager {
  @Override
  public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
      for (ConfigAttribute configAttribute : configAttributes) {
          String needRole = configAttribute.getAttribute();
          if ("ROLE_LOGIN".equals(needRole)) {
              if (authentication instanceof AnonymousAuthenticationToken) {
                  throw new BadCredentialsException("未登录");
              } else {
                  return;
              }
          }
          //遍历当前用户所具有的权限
          Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
          for (GrantedAuthority authority : authorities) {
              if (authority.getAuthority().equals(needRole)) {
                  return;
              }
          }
          //执行到这里说明没有匹配到应有权限
          throw new AccessDeniedException("权限不足!");
      }
  }

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

  @Override
  public boolean supports(Class<?> clazz) {
      return false;
  }
}
  • 加入用户访问了一个没有权限访问的路径怎么办?正常情况下浏览器会报403错误,但是我们要做的更人性化一下,所以还有个403异常Handler,功能就是返回一句话,告诉用户你没权限访问我哦!
package com.example.sptingboot_mybatis.security;

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;
import java.io.PrintWriter;

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
  @Override
  public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
      httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
      httpServletResponse.setContentType("application/json;charset=UTF-8");
      PrintWriter out = httpServletResponse.getWriter();
      out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
      out.flush();
      out.close();
  }
}

  • 至此基本辅助模块就基本完事了,但是别忘了,咱们还有验证码功能呢!
    • 首先我们写个Servlet来生成验证码
package com.example.sptingboot_mybatis.servlet;

import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

public class VerifyServlet extends HttpServlet {
  private static final long serialVersionUID = -5051097528828603895L;

  /**
   * 验证码图片的宽度。
   */
  private int width = 100;

  /**
   *  验证码图片的高度。
   */
  private int height = 30;

  /**
   * 验证码字符个数
   */
  private int codeCount = 4;

  /**
   * 字体高度
   */
  private int fontHeight;

  /**
   * 干扰线数量
   */
  private int interLine = 16;

  /**
   * 第一个字符的x轴值,因为后面的字符坐标依次递增,所以它们的x轴值是codeX的倍数
   */
  private int codeX;

  /**
   * codeY ,验证字符的y轴值,因为并行所以值一样
   */
  private int codeY;

  /**
   * codeSequence 表示字符允许出现的序列值
   */
  char[] codeSequence = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
          'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
          'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };

  /**
   * 初始化验证图片属性
   */
  @Override
  public void init() throws ServletException {
      // 从web.xml中获取初始信息
      // 宽度
      String strWidth = this.getInitParameter("width");
      // 高度
      String strHeight = this.getInitParameter("height");
      // 字符个数
      String strCodeCount = this.getInitParameter("codeCount");
      // 将配置的信息转换成数值
      try {
          if (strWidth != null && strWidth.length() != 0) {
              width = Integer.parseInt(strWidth);
          }
          if (strHeight != null && strHeight.length() != 0) {
              height = Integer.parseInt(strHeight);
          }
          if (strCodeCount != null && strCodeCount.length() != 0) {
              codeCount = Integer.parseInt(strCodeCount);
          }
      } catch (NumberFormatException e) {
          e.printStackTrace();
      }
      //width-4 除去左右多余的位置,使验证码更加集中显示,减得越多越集中。
      //codeCount+1     //等比分配显示的宽度,包括左右两边的空格
      codeX = (width-4) / (codeCount+1);
      //height - 10 集中显示验证码
      fontHeight = height - 10;
      codeY = height - 7;
  }

  /**
   * @param request
   * @param response
   * @throws ServletException
   * @throws java.io.IOException
   */
  @Override
  protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, java.io.IOException {
      // 定义图像buffer
      BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
      Graphics2D gd = buffImg.createGraphics();
      // 创建一个随机数生成器类
      Random random = new Random();
      // 将图像填充为白色
      gd.setColor(Color.LIGHT_GRAY);
      gd.fillRect(0, 0, width, height);
      // 创建字体,字体的大小应该根据图片的高度来定。
      Font font = new Font("Times New Roman", Font.PLAIN, fontHeight);
      // 设置字体。
      gd.setFont(font);
      // 画边框。
      gd.setColor(Color.BLACK);
      gd.drawRect(0, 0, width - 1, height - 1);
      // 随机产生16条干扰线,使图象中的认证码不易被其它程序探测到。
      gd.setColor(Color.gray);
      for (int i = 0; i < interLine; i++) {
          int x = random.nextInt(width);
          int y = random.nextInt(height);
          int xl = random.nextInt(12);
          int yl = random.nextInt(12);
          gd.drawLine(x, y, x + xl, y + yl);
      }
      // randomCode用于保存随机产生的验证码,以便用户登录后进行验证。
      StringBuffer randomCode = new StringBuffer();
      int red = 0, green = 0, blue = 0;
      // 随机产生codeCount数字的验证码。
      for (int i = 0; i < codeCount; i++) {
          // 得到随机产生的验证码数字。
          String strRand = String.valueOf(codeSequence[random.nextInt(36)]);
          // 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。
          red = random.nextInt(255);
          green = random.nextInt(255);
          blue = random.nextInt(255);
          // 用随机产生的颜色将验证码绘制到图像中。
          gd.setColor(new Color(red,green,blue));
          gd.drawString(strRand, (i + 1) * codeX, codeY);
          // 将产生的四个随机数组合在一起。
          randomCode.append(strRand);
      }
      // 将四位数字的验证码保存到Session中。
      HttpSession session = request.getSession();
      session.setAttribute("validateCode", randomCode.toString());
      // 禁止图像缓存。
      response.setHeader("Pragma", "no-cache");
      response.setHeader("Cache-Control", "no-cache");
      response.setDateHeader("Expires", 0);

      response.setContentType("image/jpeg");
      // 将图像输出到Servlet输出流中。
      ServletOutputStream sos = response.getOutputStream();
      ImageIO.write(buffImg, "jpeg", sos);
      sos.close();
  }
}

  • 然后把用户输入的验证码信息和生成的验证码信息进行对比,对比正确就放行,对比不正确就跳转到登录界面并提示验证码输入错误。
package com.example.sptingboot_mybatis.filter;

import org.springframework.security.authentication.DisabledException;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
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;
@Component
public class VerifyFilter extends OncePerRequestFilter {
    private static final PathMatcher pathMatcher = new AntPathMatcher();
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        if(isProtectedUrl(httpServletRequest)) {
            String verifyCode = httpServletRequest.getParameter("verifyCode");
            if(!validateVerify(verifyCode)) {
                //手动设置异常
                httpServletRequest.getSession().setAttribute("SPRING_SECURITY_LAST_EXCEPTION",new DisabledException("验证码输入错误"));
                // 转发到错误Url
                httpServletRequest.getRequestDispatcher("/login/error").forward(httpServletRequest,httpServletResponse);
            } else {
                filterChain.doFilter(httpServletRequest,httpServletResponse);
            }
        } else {
            filterChain.doFilter(httpServletRequest,httpServletResponse);
        }
    }


    private boolean validateVerify(String inputVerify) {
        //获取当前线程绑定的request对象
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 不分区大小写
        // 这个validateCode是在servlet中存入session的名字
        String validateCode = ((String) request.getSession().getAttribute("validateCode")).toLowerCase();
        inputVerify = inputVerify.toLowerCase();

//        System.out.println("验证码:" + validateCode + "用户输入:" + inputVerify);
        return validateCode.equals(inputVerify);
    }

    // 拦截 /login的POST请求
    private boolean isProtectedUrl(HttpServletRequest request) {
        return "POST".equals(request.getMethod()) && pathMatcher.match("/login", request.getServletPath());
    }
}
  • 此外还需要在启动类出配置一下才能达到验证码的功能
package com.example.sptingboot_mybatis;

import com.example.sptingboot_mybatis.servlet.VerifyServlet;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@MapperScan("com.example.sptingboot_mybatis.dao")
public class SptingbootMybatisApplication {

 public static void main(String[] args) {
     SpringApplication.run(SptingbootMybatisApplication.class, args);
 }
 /**
  * 注入验证码servlet
  */
 @Bean
 public ServletRegistrationBean indexServletRegistration() {
     ServletRegistrationBean registration = new ServletRegistrationBean(new VerifyServlet());
     registration.addUrlMappings("/getVerifyCode");
     return registration;
 }

}

10.配置WebSecurityConfig,真正的security的配置类

package com.example.sptingboot_mybatis.security;

import com.example.sptingboot_mybatis.filter.VerifyFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
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.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

@Configuration
/*标识该类是配置类*/
@EnableWebSecurity
/*开启Security服务*/
@EnableGlobalMethodSecurity(prePostEnabled = true)
/*开启全局Securtiy注解*/
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  CustomUserDetailsService customUserDetailsService;
  @Autowired
  CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
  @Autowired
  CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
  //根据一个url请求,获得访问它所需要的roles权限
  @Autowired
  FilterInvocationSecurityMetadataSourceImpl filterInvocationSecurityMetadataSource;
  //接收一个用户的信息和访问一个url所需要的权限,判断该用户是否可以访问
  @Autowired
  AccessDecisionManagerImpl accessDecisionManager;
  //403页面
  @Autowired
  AccessDeniedHandlerImpl accessDeniedHandler;
  @Autowired
  DataSource dataSource;
  @Autowired
  private CustomUserDetailsService userDetailsService;
  @Bean
  public SessionRegistry sessionRegistry() {
      return new SessionRegistryImpl();
  }

  @Bean
  public PersistentTokenRepository persistentTokenRepository(){
      JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
      tokenRepository.setDataSource(dataSource);
      // 如果token表不存在,使用下面语句可以初始化该表;若存在,会报错。
      //tokenRepository.setCreateTableOnStartup(true);
      return tokenRepository;
  }
  @Override
  /**定义认证用户信息获取来源,密码校验规则等,注释部分为明文密码验证方式,需要的话直接解除注释同时注释掉new BCryptPasswordEncoder()即可*/
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.userDetailsService(customUserDetailsService).passwordEncoder(new BCryptPasswordEncoder()
            /*  new PasswordEncoder() {
                  @Override
                  public String encode(CharSequence charSequence) {
                      return charSequence.toString();
                  }

                  @Override
                  public boolean matches(CharSequence charSequence, String s) {
                      return s.equals(charSequence.toString());
                  }
              }*/);
  }

  @Override
  /**定义安全策略*/
  protected void configure(HttpSecurity http) throws Exception {
      http.authorizeRequests()
              .antMatchers("/register","/getVerifyCode").permitAll()
              //配置安全策略,就是拦截URL,验证访问权限的功能
              .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                  @Override
                  public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                      o.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);
                      o.setAccessDecisionManager(accessDecisionManager);
                      return o;
                  }
              })
              .and()
              // 设置登陆页
              .formLogin()
              .loginPage("/login")
              //登录失败后的处理方式
              .failureHandler(customAuthenticationFailureHandler)
               //登录成功后的处理方式
              .successHandler(customAuthenticationSuccessHandler)
              .and()
              //验证码功能实现
              .addFilterBefore(new VerifyFilter(), UsernamePasswordAuthenticationFilter.class)
              //退出登录
              .logout().permitAll()
              .logoutUrl("/logout")
              .logoutSuccessUrl("/login")
              .and()
              // 关闭CSRF跨域
              .csrf().disable()
              .exceptionHandling()
              //403
              .accessDeniedHandler(accessDeniedHandler);
  }

  @Override
  //在这里配置哪些页面不需要认证
  public void configure(WebSecurity web) throws Exception {
      // 设置拦截忽略文件夹,可以对静态资源放行
      web.ignoring().antMatchers("/css/**", "/js/**");
  }

}

至此我们整个后台代码就写完了,如有问题欢迎指出。
附上前端页面吧(thymeleaf框架)!

  • login.html登录页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <title>登陆</title>
  <link rel="stylesheet" href="/css/bootstrap.css" type="text/css">
  <link rel="stylesheet" href="/css/login.css" type="text/css">
</head>
<body>
<div class="container">
  <div class="row">
      <div class="col-md-offset-3 col-md-6">
          <form class="form-horizontal" action="/login" method="post">
              <span class="heading">用户登录</span>
              <div class="form-group">
                  <input type="text" class="form-control" name="username" placeholder="用户名" required>
              </div>
              <div class="form-group help">
                  <input type="password" class="form-control" name="password" placeholder="密码" required>
              </div>
              <div class="form-group help">
                  <input type="text" class="form-control" name="verifyCode" placeholder="验证码" required>
              </div>
              <div th:if="${session.msg}">
                  <div th:text="${session.msg.getInfo()}"></div>
              </div>

              <div class="form-group">
                  <img src="getVerifyCode" title="看不清,请点我" onclick="refresh(this)" onmouseover="mouseover(this)"/>
                  <a href="/register" style="margin-left: 50px">没有账号?立即注册</a>
              </div>
              <div class="form-group">
                  <button type="submit" class="btn btn-default">登录</button>
              </div>
          </form>
      </div>
  </div>
</div>

<script src="/js/jquery-3.2.1.min.js"></script>
<script src="/js/bootstrap.js"></script>
<script>
  function refresh(obj) {
      obj.src = "getVerifyCode?" + Math.random();
  }

  function mouseover(obj) {
      obj.style.cursor = "pointer";
  }
</script>
</body>
</html>
  • register.html注册页面
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <title>注册</title>
  <link rel="stylesheet" href="/css/bootstrap.css">
  <link rel="stylesheet" href="/css/login.css">
</head>
<body>
<div class="container">
  <div class="row">
      <div class="col-md-offset-3 col-md-6">
          <form class="form-horizontal" method="post" action="/register">
              <span class="heading">用户注册</span>
              <div class="form-group">
                  <input type="text" class="form-control" name="name" placeholder="用户名" required>
              </div>
              <div class="form-group help">
                  <input type="password" class="form-control" name="password" placeholder="密码" required>
              </div>
              <div class="form-group help">
                  <input type="password" class="form-control" name="password1" placeholder="确认密码" required>
              </div>
              <div class="form-group help">
                  <label class="checkbox-inline">选择权限(可多选):</label>
                  <label class="checkbox-inline">
                      <input type="checkbox" name="roles" value="1"> admin
                  </label>
                  <label class="checkbox-inline">
                      <input type="checkbox" name="roles" value="2"> user
                  </label>
              </div>
              <div class="form-group">
                  <button class="btn btn-default" onclick="window.location.href='/login'" style="float: left;">登陆</button>
                  <button class="btn btn-default" type="submit">注册</button>
              </div>
          </form>
      </div>
  </div>
</div>

<script src="/js/jquery-3.2.1.min.js"></script>
<script src="/js/bootstrap.js"></script>
</body>
</html>
  • home.html登录成功跳转页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<h1>登陆成功</h1>
<h3>拥有权限:<span th:text="${session.msg.getData()}"></span></h3>
<div>
  <a href="/admin">检测ROLE_ADMIN角色</a>
</div>
<div>
  <a href="/user">检测ROLE_USER角色</a>
</div>
<div>
  <a href="/all">检测通用角色</a>

</div>

<button onclick="window.location.href='/logout'">退出登录</button>

</body>
</html>

文章结尾再次感谢以下两位大神的文章,我只不过是做了融合:
(1)【详细】Spring Boot框架整合Spring Security实现安全访问控制
(2) SpringBoot集成Spring Security(1)——入门程序

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值