基础开发框架搭建3—springbooot集成spring security实现登录及权限控制
spring security:
spring security是一个安全登录框架,常用于用户登录、注销、角色权限控制、页面接口访问控制等等,功能强大且上手较难,最近搭建基础开发框架简单集成一下。目前大多数教程都比较老旧,仍然使用了模板引擎、jsp相关的技术,这不符合我们目前的日常开发现状。这篇博客后端使用springboot2.x+mybatis,前端只使用了2个简单的html页面,比较易于理解security。
这篇博客想用spring security实现的比较简单的事情:
- 用户登录和退出,即用户登录、退出的相关操作,当然包含session相关的操作,我不想自己做,我想让spring security帮我做。
- 页面、restful接口的权限控制,我不想让非ADMIN的用户去访问到/admin/**相关的页面和接口,我想让spring security帮我拦截这些请求并返回权限不足/未登录登信息。
- 当然spring security的功能远远不止这些,这边博客仅仅是简单集成,后面有机会会集成更多功能。
github地址:
https://github.com/hellozhaoxudong/springbootMerge
1.表结构和实体类
(1)相关表结构信息
- 用户信息表sys_user
- 角色信息表sys_role
注意下这里面的角色编码都要加上ROLE_前缀,因为security进行拦截后角色对比时会自动加上ROLE_前缀,此处若想深入研究请阅读源码。
- 用户-角色信息关联表sys_user_role
(2)实体类
- 用户信息实体
/**
* @ClassName SysUser
* @description 用户信息实体
* @author hellozhaoxudong@163.com
* @date 2019/4/28 15:50
* @version 1.0
* @since JDK 1.8
*/
@Table(name = "sys_user")
public class SysUser {
/** 用户ID **/
@Id
private Long userId;
/** 登录名 **/
@Column
private String username;
/** 密码 **/
@Column
private String password;
/** 用户真实姓名 **/
@Column
private String userRealName;
/** 手机号 **/
@Column
private String phone;
/** 邮箱 **/
@Column
private String email;
/** 信用值 **/
@Column
private Long credit;
/** getter & setter **/
}
- 角色信息实体
/**
* @ClassName SysRole
* @description 角色信息实体
* @author hellozhaoxudong@163.com
* @date 2019/4/28 15:57
* @version 1.0
* @since JDK 1.8
*/
@Table(name = "sys_role")
public class SysRole {
/** 角色ID **/
@Id
private Long roleId;
/** 角色编码 **/
@Column
private String roleCode;
/** 角色名称 **/
@Column
private String roleName;
/** setter & getter **/
}
- 用户-角色关联信息实体
/**
* @ClassName SysUserRole
* @description 用户角色实体
* @author hellozhaoxudong@163.com
* @date 2019/4/28 16:00
* @version 1.0
* @since JDK 1.8
*/
@Table(name = "sys_user_role")
public class SysUserRole {
/** 用户角色信息ID **/
@Id
private Long userRoleId;
/** 用户ID **/
@Column
private Long userId;
/** 角色ID **/
@Column
private Long roleId;
/** 角色编码 **/
@Transient
private String roleCode;
/** 角色名称 **/
@Transient
private String roleName;
/** setter & getter **/
}
2.根据用户名获取用户信息、根据用户ID获取用户角色信息两个逻辑代码
(1)根据用户名获取用户信息
- SysUserService.java
/**
* @ClassName SysUserService
* @description 用户信息逻辑层
* @author hellozhaoxudong@163.com
* @date 2019/4/28 16:14
* @version 1.0
* @since JDK 1.8
*/
@Service
public class SysUserService {
@Autowired
private SysUserMapper mapper;
/**
* loadUserByUsername : 根据用户名查询用户信息
*
* @author hellozhaoxudong@163.com
* @version 1.0
* @date 2019/4/28 16:21
* @return java.util.List<com.spboot.merge.user.dto.SysUser>
* @since JDK 1.8
*/
public SysUser loadUserByUsername(String username){
SysUser queryUser = new SysUser();
queryUser.setUsername(username);
List<SysUser> sysUserList = mapper.select(queryUser);
if(null == sysUserList || sysUserList.size() == 0){
throw new BaseException("用户名不存在!");
}else if(sysUserList.size() > 1){
throw new BaseException("用户名存在冲突!");
}
return sysUserList.get(0);
}
}
- SysUserMapper.java
这里继承了通用的Mapper插件tk.mybatis.mapper.common.Mapper,所以基础的增删改查不需要自己定义。集成通用插件的过程请参考该项目git中的代码注释。
/**
* @ClassName SysUserMapper
* @description 用户信息Mapper
* @author hellozhaoxudong@163.com
* @date 2019/4/28 16:14
* @version 1.0
* @since JDK 1.8
*/
public interface SysUserMapper extends Mapper<SysUser>, InsertListMapper<SysUser> {
}
(2)根据用户ID获取用户角色信息
- SysUserRoleService.java
/**
* @ClassName SysUserRoleService
* @description 用户角色关联信息逻辑处理层
* @author hellozhaoxudong@163.com
* @date 2019/4/28 16:38
* @version 1.0
* @since JDK 1.8
*/
@Service
public class SysUserRoleService {
@Autowired
private SysUserRoleMapper mapper;
/**
* querySysUserRoleByUserId : 根据用户id查询角色信息
*
* @author hellozhaoxudong@163.com
* @version 1.0
* @date 2019/4/28 16:56
* @return java.util.List<com.spboot.merge.user.dto.SysUserRole>
* @since JDK 1.8
*/
public List<SysUserRole> querySysUserRoleByUserId(Long userId){
List<SysUserRole> list = mapper.querySysUserRoleByUserId(userId);
if(null == list || list.size() < 1){
throw new BaseException("用户下无任何角色!请联系开发者");
}
return list;
}
}
- SysUserRoleMapper.java
/**
* @ClassName SysUserRoleMapper
* @description 用户角色信息Mapper
* @author hellozhaoxudong@163.com
* @date 2019/4/28 16:36
* @version 1.0
* @since JDK 1.8
*/
public interface SysUserRoleMapper extends Mapper<SysUserRole>, InsertListMapper<SysUserRole> {
/**
* querySysUserRoleByUserId : 根据用户id查询角色信息
*
* @author hellozhaoxudong@163.com
* @version 1.0
* @date 2019/4/28 16:51
* @return java.util.List<com.spboot.merge.user.dto.SysUserRole>
* @since JDK 1.8
*/
List<SysUserRole> querySysUserRoleByUserId(@Param("userId") Long userId);
}
- SysUserRoleMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.spboot.merge.user.mapper.SysUserRoleMapper">
<resultMap id="BaseMapper" type="com.spboot.merge.user.dto.SysUserRole">
<result column="user_role_id" property="userRoleId" jdbcType="INTEGER"></result>
<result column="user_id" property="userId" jdbcType="INTEGER"></result>
<result column="role_id" property="roleId" jdbcType="INTEGER"></result>
<result column="role_code" property="roleCode" jdbcType="VARCHAR"></result>
<result column="role_name" property="roleName" jdbcType="VARCHAR"></result>
</resultMap>
<!--根据用户id查询角色信息-->
<select id="querySysUserRoleByUserId" resultType="com.spboot.merge.user.dto.SysUserRole" resultMap="BaseMapper">
select sur.user_role_id,
sur.user_id,
sur.role_id,
sr.role_code,
sr.role_name
from sys_user_role sur, sys_role sr
where sur.role_id = sr.role_id
and sur.user_id = #{userId}
</select>
</mapper>
3.spring security相关配置
(1)pom文件中引入依赖
<!--Spring Security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
(2)实现UserDetailsService
自己写一个类实现UserDetailsService,重写loadUserByUsername方法,作用就是我们自己去组装登录用户的用户名、密码、角色集信息并在配置类中注入给security。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysUserRoleService sysUserRoleService;
/**
* loadUserByUsername : 获取用户信息(用户名,密码,角色集)
*
* 返回UserDetails,这是一个接口,通常返回它的字类org.springframework.security.core.userdetails.User
* User的构造需要三个参数:用户名,密码,角色集
*
* @author hellozhaoxudong@163.com
* @version 1.0
* @date 2019/4/29 14:36
* @return org.springframework.security.core.userdetails.UserDetails
* @since JDK 1.8
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 用户信息
SysUser sysUser = sysUserService.loadUserByUsername(username);
// 角色信息
List<SysUserRole> sysUserRoleList = sysUserRoleService.querySysUserRoleByUserId(sysUser.getUserId());
List<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>();
SimpleGrantedAuthority simpleGrantedAuthority;
for(SysUserRole sysUserRole : sysUserRoleList){
simpleGrantedAuthority = new SimpleGrantedAuthority(sysUserRole.getRoleCode());
authorities.add(simpleGrantedAuthority);
}
return new User(sysUser.getUsername(), sysUser.getPassword(), authorities);
}
}
(3)spring security配置类
@Configuration // 标识该类为配置类
@EnableWebSecurity // 开启Spring Security服务
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启全局Spring Security注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
// 指定用户,角色信息加载及密码加密方式,将默认的userDetailsService替换成我们自己实现的UserDetailsServiceImpl
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(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("/login.html","/api/login", "/visitor/**").permitAll() // "/login.html"是登录页面,"/api/login"是登录url, "/visitor/**"游客访问相关的页面和接口所以我们直接不拦截
.antMatchers("/admin/**").hasAnyRole("ADMIN") // "/admin/ \*\*"相关的页面和接口,只能ADMIN角色访问。
.anyRequest().authenticated(); // 其他所有的页面和请求登录成功后才可以访问
// 配置登录相关信息
http.formLogin()
.loginPage("/login.html") // 指定登录页面(当我们未登录状态访问一个页面时会自动跳转到此处指定的页面)
.loginProcessingUrl("/api/login") // 指定登录url(默认为/login,此处和表单的action要一致,即登录信息提交到此处指定的url后security才可进行登录处理)
.usernameParameter("username") // 指定登录用户名参数名称(默认为username)
.passwordParameter("password") // 指定密码参数名称(默认为password)
.defaultSuccessUrl("/index.html") // 指定登录成功后跳转页
.failureUrl("/api/login/error") // 指定登录失败URL,可在该接口中抛出异常。(默认为/login?error,就是在controller中写一个接口,登录失败会跳转那个接口)
.permitAll();
// 配置注销相关信息
http.logout()
.logoutUrl("/api/loginout") // 指定注销URL(默认为/login,即需要注销时需要调用此处指定的url后security才可进行注销处理)
.permitAll();
// 关闭CSRF跨域
http.csrf().disable();
}
// 配置对资源文件放行
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/js/**");
}
}
4.登录页面、主页
(1)登录页面login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆</title>
</head>
<body>
<h1>登陆</h1>
<form method="post" action="/api/login">
<div>
用户名:<input type="text" name="username">
</div>
<div>
密码:<input type="password" name="password">
</div>
<div>
<button type="submit">立即登陆</button>
</div>
</form>
</body>
</html>
(2)主页index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>欢迎</title>
</head>
<body>
<h1>登陆成功</h1>
</body>
</html>
5.测试
需要测试:登录功能是否正常使用?html页面是否可以正确被拦截?接口是否可以正确被拦截?
(1)页面测试
admin/securitytest.html应该是只有角色为ADMIN的用户登录后才能访问。
visitor/securitytest.html无论登不登录都可以随意访问。
(2)URL测试
@RestController
@RequestMapping("/")
public class SecurityTestController {
/**
* baseTest : Spring Security 基础测试:未登录时访问该接口应该自动转到login.html,登录后访问该接口应返回“springsecurity”
*
* @author hellozhaoxudong@163.com
* @version 1.0
* @date 2019/5/6 15:02
* @return java.lang.String
* @since JDK 1.8
*/
@GetMapping(value = "securitytest/basetest")
public String baseTest(){
return "spring security";
}
/**
* adminSecurityTest : API测试:未登录时访问该接口应该自动转到login.html,非管理员角色登录后应提示403权限不足,管理员登录后应返回“api测试:admin”
*
* @author hellozhaoxudong@163.com
* @version 1.0
* @date 2019/5/6 15:11
* @return java.lang.String
* @since JDK 1.8
*/
@GetMapping(value = "admin/securitytest")
public String adminSecurityTest(){
return "api测试:admin";
}
}
可以使用POSTMAN发送GET请求进行测试。
具体的测试可以在github下载该项目进行测试和使用,此处只放两张简单的图!