在上一篇文章中:Spring Security入门教程:利用Spring Security实现安全控制
我们学会了使用Spring Security实现安全控制,学会了他的基础使用,这节课我们来学习一下它的自定义的功能,更深入的了解和使用Spring Security。
登录成功失败和注销
在Spring Security中我们可以自己实现登录成功跟登录失败之后我们系统做的事情。首先我们定义一个登陆失败的类。我们需要实现一个AuthenticationFailureHandler 接口,并且重写他的onAuthenticationFailure方法。
package com.masiyi.springsecuritydemo.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
Map<String, Object> result = new HashMap<String, Object>();
result.put("msg", "登录失败: "+exception.getMessage());
result.put("status", 500);
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}
}
例如在我们这个上面,我们给要用方返回一个登录失败的提示信息。
之后我们在自定义一个登录成功的处理类。他跟上面的登陆失败的。写法类似,也是实现一个AuthenticationSuccessHandler 接口,在这个方法里面。我们给调用端返回一个登录成功的信息。
package com.masiyi.springsecuritydemo.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> result = new HashMap<String, Object>();
result.put("msg", "登录成功");
result.put("status", 200);
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}
}
当然类似的还有退出成功的处理类
package com.masiyi.springsecuritydemo.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> result = new HashMap<String, Object>();
result.put("msg", "注销成功");
result.put("status", 200);
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}
}
订完这三个自定义的类之后,我们需要给它配置。这样的话,spring security就知道我们需要他做一些什么事情。我们写一个配置类如下。我们刚刚写好的自定义的处理类给他set值进去。这样的话我们再进行一个登录成功,登录失败跟退出操作。他就会执行我们自定义类里面的逻辑。
package com.masiyi.springsecuritydemo.config;
import com.masiyi.springsecuritydemo.handler.MyAuthenticationFailureHandler;
import com.masiyi.springsecuritydemo.handler.MyAuthenticationSuccessHandler;
import com.masiyi.springsecuritydemo.handler.MyLogoutSuccessHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/user").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.successHandler(new MyAuthenticationSuccessHandler())
.failureHandler(new MyAuthenticationFailureHandler())
.and()
.logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
;
}
}
获取登录成功的用户信息
@RestController
public class BasicController {
@RequestMapping("/auth")
public String hello() {
//org.springframework.security.web.context.HttpRequestResponseHolder 关键方法
Authentication authentication = SecurityContextHolder
.getContext().getAuthentication();
org.springframework.security.core.userdetails.User principal = (org.springframework.security.core.userdetails.User) authentication.getPrincipal();
System.out.println("身份 :"+principal.getUsername());
System.out.println("凭证 :"+authentication.getCredentials());
System.out.println("权限 :"+authentication.getAuthorities());
return "hello security";
}
}
SecurityContextHolder.getContext() 获取Spring Security的上下文,上下文中存储着登录的用户信息Authentication ,利用SecurityContextHolder.getContext().getAuthentication()
即可获取
自定义内存用户
我们在WebSecurityConfigurer里面添加如下代码,之前我们都是在yml文件中配置用户和密码,这样的话我们可以在代码中使用内存中的用户信息
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager
= new InMemoryUserDetailsManager();
UserDetails u1 = User.withUsername("masiyi")
.password("{noop}111").roles("USER").build();
inMemoryUserDetailsManager.createUser(u1);
return inMemoryUserDetailsManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
自定义数据源用户
在实际工作和项目中我们肯定不会和上门一样把用户和密码写死,大部分我们是把用户信息存储在数据库中的,所以我们就需要学习在Spring Security中自定义数据源用户,这里以mysql数据库为例
首先在pom文件中引入以下jar包:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.7</version>
</dependency>
接着在配置文件中加入你的数据库连接信息:
# datasource
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springsecurity?characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
# mybatis
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.masiyi.springsecuritydemo.entity
在数据库中执行以下sql以创建对应的数据表
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 50738
Source Host : localhost:3306
Source Schema : springsecurity
Target Server Type : MySQL
Target Server Version : 50738
File Encoding : 65001
Date: 20/12/2023 19:47:32
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`name_zh` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'ROLE_product', '商品管理员');
INSERT INTO `role` VALUES (2, 'ROLE_admin', '系统管理员');
INSERT INTO `role` VALUES (3, 'ROLE_user', '用户管理员');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`enabled` tinyint(1) DEFAULT NULL,
`accountNonExpired` tinyint(1) DEFAULT NULL,
`accountNonLocked` tinyint(1) DEFAULT NULL,
`credentialsNonExpired` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'root', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user` VALUES (2, 'admin', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user` VALUES (3, 'blr', '{noop}123', 1, 1, 1, 1);
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `uid`(`uid`) USING BTREE,
INDEX `rid`(`rid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 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, 1, 2);
INSERT INTO `user_role` VALUES (3, 2, 2);
INSERT INTO `user_role` VALUES (4, 3, 3);
SET FOREIGN_KEY_CHECKS = 1;
根据user表新建一个对应的实体类,但是这个类要实现UserDetails 接口
package com.masiyi.springsecuritydemo.entity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;
private List<Role> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
roles.forEach(role -> grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));
return grantedAuthorities;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public Boolean getAccountNonExpired() {
return accountNonExpired;
}
public void setAccountNonExpired(Boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public Boolean getAccountNonLocked() {
return accountNonLocked;
}
public void setAccountNonLocked(Boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public Boolean getCredentialsNonExpired() {
return credentialsNonExpired;
}
public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
}
Role 角色类
package com.masiyi.springsecuritydemo.entity;
public class Role {
private Integer id;
private String name;
private String nameZh;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNameZh() {
return nameZh;
}
public void setNameZh(String nameZh) {
this.nameZh = nameZh;
}
}
新建一个xml文件对应user
<?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.masiyi.springsecuritydemo.dao.UserDao">
<!--查询单个-->
<select id="loadUserByUsername" resultType="User">
select id,
username,
password,
enabled,
accountNonExpired,
accountNonLocked,
credentialsNonExpired
from user
where username = #{username}
</select>
<!--查询指定行数据-->
<select id="getRolesByUid" resultType="Role">
select r.id,
r.name,
r.name_zh nameZh
from role r,
user_role ur
where r.id = ur.rid
and ur.uid = #{uid}
</select>
</mapper>
UserDao 接口
package com.masiyi.springsecuritydemo.dao;
import com.masiyi.springsecuritydemo.entity.Role;
import com.masiyi.springsecuritydemo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserDao {
//根据用户名查询用户
User loadUserByUsername(String username);
//根据用户id查询角色
List<Role> getRolesByUid(Integer uid);
}
MyUserDetailService类,实现org.springframework.security.core.userdetails.UserDetailsService
接口,重写loadUserByUsername方法。
package com.masiyi.springsecuritydemo.service;
import com.masiyi.springsecuritydemo.dao.UserDao;
import com.masiyi.springsecuritydemo.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;
import org.springframework.util.ObjectUtils;
@Component
public class MyUserDetailService implements UserDetailsService {
private final UserDao userDao;
@Autowired
public MyUserDetailService(UserDao userDao) {
this.userDao = userDao;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.loadUserByUsername(username);
if (ObjectUtils.isEmpty(user)) {
throw new RuntimeException("用户不存在");
}
user.setRoles(userDao.getRolesByUid(user.getId()));
return user;
}
}
在WebSecurityConfigurer类中把
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager
= new InMemoryUserDetailsManager();
UserDetails u1 = User.withUsername("masiyi")
.password("{noop}111").roles("USER").build();
inMemoryUserDetailsManager.createUser(u1);
return inMemoryUserDetailsManager;
}
注释掉,用下面的配置
@Autowired
private MyUserDetailService myUserDetailService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService);
}
这样,就可以读取自定义的用户了,即自定义数据源用户。
注册,加密密码,登录解密密码
在上面我们是直接初始化用户的,我们并没有添加用户的功能,但是在实际项目当中,肯定要有注册的功能,所以我们给前端写一个注册的方法
package com.masiyi.springsecuritydemo.controller;
import com.masiyi.springsecuritydemo.service.MyUserDetailService;
import com.masiyi.springsecuritydemo.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author masiyi
* @Date 2023/12/20
* @PackageName:com.masiyi.springsecuritydemo.controller
* @ClassName: UserController
* @Description: TODO
* @Version 1.0
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private MyUserDetailService myUserDetailService;
@PostMapping("/register")
public Boolean register(@RequestBody User userInfo){
return myUserDetailService.insertUser(userInfo);
}
}
MyUserDetailService 类中我们的insertUser方法如下
import com.masiyi.springsecuritydemo.dao.UserDao;
import com.masiyi.springsecuritydemo.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
@Component
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UserDao userDao;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.loadUserByUsername(username);
if (ObjectUtils.isEmpty(user)) {
throw new RuntimeException("用户不存在");
}
user.setRoles(userDao.getRolesByUid(user.getId()));
return user;
}
public Boolean insertUser(User userInfo) {
//登录的时候在 UsernamePasswordAuthenticationFilter里面解密
userInfo.setPassword(passwordEncoder.encode(userInfo.getPassword()));
return userDao.insertUser(userInfo);
}
}
对应的xml文件的sql如下:
<insert id="insertUser">
insert into user(username,password) values (#{userInfo.username},#{userInfo.password})
</insert>
配置类要改的内容如下
package com.masiyi.springsecuritydemo.config;
import com.masiyi.springsecuritydemo.handler.MyAuthenticationFailureHandler;
import com.masiyi.springsecuritydemo.handler.MyAuthenticationSuccessHandler;
import com.masiyi.springsecuritydemo.handler.MyLogoutSuccessHandler;
import com.masiyi.springsecuritydemo.service.MyUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
/**
* 这里有两种方式 authorizeHttpRequests 和 authorizeRequests
* @param http the {@link HttpSecurity} to modify
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/user/register").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.successHandler(new MyAuthenticationSuccessHandler())
.failureHandler(new MyAuthenticationFailureHandler())
.and()
.logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
.and().csrf().disable()
;
}
//
// @Bean
// public UserDetailsService userDetailsService() {
// InMemoryUserDetailsManager inMemoryUserDetailsManager
// = new InMemoryUserDetailsManager();
// UserDetails u1 = User.withUsername("masiyi")
// .password("{noop}111").roles("USER").build();
// inMemoryUserDetailsManager.createUser(u1);
// return inMemoryUserDetailsManager;
// }
@Bean
public UserDetailsService userDetailsService() {
return new MyUserDetailService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
/**
* 指定加密方式
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCrypt加密密码
return new BCryptPasswordEncoder();
}
}
至此spring security实现自定义用户配置,我们就讲到这里。下面我会给大家讲一下spring security的授权的功能。
项目的地址就在
https://gitee.com/WangFuGui-Ma/spring-security-admin
如果大家对这篇文章或者专栏有兴趣或者对大家有所帮助的话,欢迎关注点赞。加评论。
我们spring security的进阶专栏见。