在上一篇文章中: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 {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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 {@Overrideprotected 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文件中配置用户和密码,这样的话我们可以在代码中使用内存中的用户信息
@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager inMemoryUserDetailsManager= new InMemoryUserDetailsManager();UserDetails u1 = User.withUsername("masiyi").password("{noop}111").roles("USER").build();inMemoryUserDetailsManager.createUser(u1);return inMemoryUserDetailsManager;}@Overrideprotected 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 TransferSource Server : localhostSource Server Type : MySQLSource Server Version : 50738Source Host : localhost:3306Source Schema : springsecurityTarget Server Type : MySQLTarget Server Version : 50738File Encoding : 65001Date: 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<>();@Overridepublic 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;}@Overridepublic String getUsername() {return username;}@Overridepublic String getPassword() {return password;}@Overridepublic boolean isAccountNonExpired() {return accountNonExpired;}@Overridepublic boolean isAccountNonLocked() {return accountNonLocked;}@Overridepublic boolean isCredentialsNonExpired() {return credentialsNonExpired;}@Overridepublic 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,credentialsNonExpiredfrom userwhere username = #{username}</select><!--查询指定行数据--><select id="getRolesByUid" resultType="Role">select r.id,r.name,r.name_zh nameZhfrom role r,user_role urwhere r.id = ur.ridand 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;@Autowiredpublic MyUserDetailService(UserDao userDao) {this.userDao = userDao;}@Overridepublic 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类中把
@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager inMemoryUserDetailsManager= new InMemoryUserDetailsManager();UserDetails u1 = User.withUsername("masiyi").password("{noop}111").roles("USER").build();inMemoryUserDetailsManager.createUser(u1);return inMemoryUserDetailsManager;}
注释掉,用下面的配置
@Autowiredprivate MyUserDetailService myUserDetailService;@Overrideprotected 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 {@Autowiredprivate 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 {@Autowiredprivate UserDao userDao;@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic 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*/@Overrideprotected 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;
// }@Beanpublic UserDetailsService userDetailsService() {return new MyUserDetailService();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());}/*** 指定加密方式*/@Beanpublic PasswordEncoder passwordEncoder() {// 使用BCrypt加密密码return new BCryptPasswordEncoder();}
}
至此spring security实现自定义用户配置,我们就讲到这里。下面我会给大家讲一下spring security的授权的功能。
项目的地址就在
https://gitee.com/WangFuGui-Ma/spring-security-admin
如果大家对这篇文章或者专栏有兴趣或者对大家有所帮助的话,欢迎关注点赞。加评论。
我们spring security的进阶专栏见。