**本人第一次写博客,代码如有错误之处,烦请指出,十分感谢!话不多说,进入正题:**
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)——入门程序