文章目录
一、前言
权限认证框架最常见的除了Shiro,另一个就是Spring Security,相比Shiro而言Spring Security学习难度较大,这里我通过博客记录下自己学习Spring Security的历程。
如果您跟随博客学习,请尽量保持环境一致,否则掉坑了不负责哈O(∩_∩)O~
环境:SpringBoot2.3.6.RELEASE,Spring Security5+
二、环境依赖
这里使用的SpringBoot是2.3.6,Spring Security默认是5.3.5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
三、数据库
这里开始简单点,只涉及到三张表,user、role、user_role,先不处理权限问题。
- user表
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'admin', '123');
INSERT INTO `sys_user` VALUES (2, 'customer', '111');
- role表
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'ROLE_admin');
INSERT INTO `role` VALUES (2, 'ROLE_customer');
PS:角色名是‘ROLE_xxx’格式,下面做说明
- user_role表
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NULL DEFAULT NULL,
`role_id` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (2, 2, 2);
四、代码
1、配置文件
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3307/security?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
#开启Mybatis下划线命名转驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true
logging.level.com.lr.security.dao=debug
2、model
数据表对应的java实体类我这里就不贴出来了,没啥特别的地方。
3、dao
- SysUserMapper
@Mapper
public interface SysUserMapper {
@Select("select * from user where id = #{id}")
SysUser selectById(Integer id);
@Select("select * from user where username = #{username}")
SysUser selectByUserName(String username);
}
- RoleMapper
@Mapper
public interface RoleMapper {
@Select("select * from role where id = #{id}")
Role selectById(Integer id);
@Select("select * from role where role_name = #{roleName}")
Role selectByRoleName(String roleName);
}
- UserRoleMapper
@Mapper
public interface UserRoleMapper {
@Select("select * from user_role where user_id = #{userId}")
List<UserRole> selectByUserId(Integer userId);
}
4、service
- SysUserService
@Service
public class SysUserService {
@Autowired
private SysUserMapper userMapper;
public SysUser selectById(Integer id) {
return userMapper.selectById(id);
}
public SysUser selectByName(String username) {
return userMapper.selectByUserName(username);
}
}
- RoleService
@Service
public class RoleService {
@Autowired
private RoleMapper roleMapper;
public Role selectById(Integer id){
return roleMapper.selectById(id);
}
}
- UserRoleService
@Service
public class UserRoleService {
@Autowired
private UserRoleMapper userRoleMapper;
public List<UserRole> listByUserId(Integer userId) {
return userRoleMapper.selectByUserId(userId);
}
}
5、controller
@Controller
public class LoginController {
@RequestMapping("/loginPage")
public String login(){
return "login.html";
}
@RequestMapping("/main")
public String toMain(Authentication authentication){ // 会自动注入Authentication
System.out.println("登录用户:" + authentication.getName());
return "main.html";
}
@PreAuthorize("hasRole('ROLE_admin')")
@RequestMapping("/admin")
@ResponseBody
public String admin(){
return "你是管理员角色";
}
@PreAuthorize("hasRole('ROLE_customer')")
@RequestMapping("/customer")
@ResponseBody
public String customer(){
return "你是普通用户角色";
}
}
1.在toMain方法中有个参数authentication,这个对象包含认证后用户的一些信息,并且可注入到springmvc
2.@PreAuthorize注解表示在执行该注解标识的方法前,需要认证。这里调用hasRole方法进行角色判断,参数即为该方法为哪些角色可执行,这里也解释下上面建表时角色名称格式问题。默认情况下未做其他配置时,hasRole方法的源码如下:
从源码可看出在判断角色名称时,是带有一个默认前缀“ROLE_”,同时从4可以看出其实在注解中我们可以省略掉前缀,因为最终也会给加上。
6、核心:Security的配置类
6.1 LoginUserDetailsService
在给Spring Security配置前还需定义用于认证用户详情的类LoginUserDetailsService,实现UserDetailsService接口
@Service
public class LoginUserDetailsService implements UserDetailsService {
@Autowired
private SysUserService userService;
@Autowired
private RoleService roleService;
@Autowired
private UserRoleService userRoleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 通过用户名查询数据库
SysUser sysUser = userService.selectByName(username);
if (sysUser == null) throw new UsernameNotFoundException("用户名不存在");
List<GrantedAuthority> authorities = new ArrayList<>();
List<UserRole> userRoles = userRoleService.listByUserId(sysUser.getId());
for (UserRole userRole : userRoles) {
Role role = roleService.selectById(userRole.getRoleId());
//SimpleGrantedAuthority实现了GrantedAuthority
authorities.add(new SimpleGrantedAuthority(role.getRoleName())); // 角色的信息
}
// UserDetails有个实现类User
// LoginUser可参照User,自己定义一个UserDetails,可便于后期拓展,若不想麻烦那就直接用Security提供的User
// return new User(sysUser.getUsername(),sysUser.getPassword(),authorities);
return new LoginUser(sysUser,authorities);
}
}
/**
* 登录用户身份权限
*/
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
private SysUser sysUser;
private List<GrantedAuthority> authorities;
public LoginUser(SysUser sysUser, List<GrantedAuthority> authorities) {
this.sysUser = sysUser;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return sysUser.getPassword();
}
@Override
public String getUsername() {
return sysUser.getUsername();
}
/**
* 账户是否未过期,过期无法验证
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 指定用户是否解锁,锁定的用户无法进行身份验证
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用 ,禁用的用户不能身份验证
*/
@Override
public boolean isEnabled() {
return true;
}
}
6.2 重点:SecurityConfig
创建Spring Security的配置类SecurityConfig,继承WebSecurityConfigurerAdapter。
1、配置类上添加注解:@EnableGlobalMethodSecurity(prePostEnabled = true),表示开启spring方法级安全,prePostEnabled = true表示启用@PreAuthorize 和@PostAuthorize 两个注解,controller中有用到的
2、引入上面创建的LoginUserDetailsService,同时重写configure方法。
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService loginUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// CSRF(跨站请求伪造)禁用,默认开启,会检测请求中是否包含令牌,没有则拒绝并返回403,我们现在不考虑这个,所以要禁用掉
.csrf().disable()
.authorizeRequests()
// 对静态资源放行(示例)
//.mvcMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js") .permitAll()
// 指定可匿名访问的路径(示例)
//.mvcMatchers("xxx","zzz/yyy").anonymous()
.anyRequest().authenticated()// 除了上面其他都必须鉴权
.and()
.formLogin().loginPage("/loginPage")// 未认证时访问跳转登录页面
.loginProcessingUrl("/doLogin")// 表单登录url设置,默认/login
// 登录成功跳转
.defaultSuccessUrl("/main",true)
.permitAll()
.and()
.logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(loginUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
PasswordEncoder passwordEncoder(){
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
};
}
}
在passwordEncoder(passwordEncoder())
中指定了密码的校验,在passwordEncoder()中的matches指明了当前密码是直接比较。因为我们数据库是存的明文,但是Security默认密码编码器都是有加密的,这个具体详情可在AuthenticationConfiguration类中查看,这里不做陈述。所以我们当前明文的话就需要自己制定下密码校验规则。
7、页面
页面直接放在static目录下,如果引入的thymeleaf依赖,这页面需要放在templates目录下
注意几点:
1、form的提交地址要与SecurityConfig 指定的一致,且必须是POST请求
2、用户名、密码的名称必须为username、password,这个是security中默认的,可在UsernamePasswordAuthenticationFilter看到。当然这个也是可以在配置类中自定定义名称
- login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/doLogin" method="post">
<input name="username"/>
<input name="password" type="password"/>
<button type="submit">登录</button>
</form>
</body>
</html>
- main.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录成功,
<a href="/admin">admin</a>
<a href="/customer">customer</a>
</body>
</html>