Java学习笔记-Day88 Spring Security安全框架
一、Spring Security的简介
Spring Security正是Spring 家族中的成员,Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI和AOP功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
应用的安全性包括 用户认证(Authentication) 和 用户授权(Authorization) 两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
Spring Security的主要核心功能为认证和授权,整个架构也是基于这两个核心功能去实现的。
二、Spring Security和Shiro比较
Spring Security 的特点:
(1)和Spring无缝整合。
(2)全面的权限控制。
(3)专门为Web开发而设计。
(4)旧版本不能脱离Web环境使用。
(5)新版本对整个框架进行了分层抽取,分成了核心模块和Web 模块。单独引入核心模块就可以脱离Web环境。
(6)重量级。
Shiro 的特点:
(1)轻量级。Shiro主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现。
(2)通用性。
(3)好处:不局限于Web环境,可以脱离Web环境使用。
(4)缺陷:在Web环境下一些特定的需求需要手动编写代码定制。
Shiro和Security相比,各有千秋,在 SSM 中整合 Spring Security 相对麻烦,所以 Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多,很多场景中,Shiro也够用。Spring Boot 横空出世后,Spring Boot 对于 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用 Spring Security。
因此,常见的安全管理技术栈的组合是这样的:
- SSM + Shiro、JSP + Servlet + Shiro
- Spring Boot + Spring Security、Spring Cloud + Spring Security
- Spring Boot + Shiro +Jwt、Spring Cloud + Shiro +Jwt
三、Spring Security的使用
只导入Spring Security 依赖,不进行任何配置的话,登录默认使用的用户名是 user, 密码则是随机生成并使用UUID加密后的密码(会在控制台输出)。这里我们会修改成使用数据库中用户表的用户名和密码进行登录,数据库表中的密码存储的是使用UUID加密后的密码。
步骤:
(1)点击 New Project -> 选择Spring Initializr -> 点击Next -> 输入Group和Artifact,Java Version选择8 ->点击Next。
(2)选择Web的Spring Web、Security的Spring Security、SQL的Mybatis Framework和MySQL Driver。
(3)输入Project name和Project location,点击 Finish 完成。
(4)修改配置文件application.properties。
server.port=8081
logging.level.org.springframework.security=trace
#数据库连接配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/zmalldb?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
#指定mybatis中配置的映射文件的地址
mybatis.mapper-locations=classpath:/mapper/*.xml
(5)将idea连接MySQL数据库,点击idea右侧的Database -> 点击 + 号 -> 选择Data Source -> 选择MySQL。
在General选项页输入IP地址、端口号、账号、密码、数据库名。点击Test Connecton,会出现时区错误,点击set time zone设置时区。将servertimezone修改为Asia/Shanghai。
添加成功后,会在右侧显示如下界面。
(6)选择tbl_user表右键点击,使用 MyBatisX-Generator 插件生成tbl_user表的entity类和Mapper类。在 LoginUserMapper.java 和 LoginUserMapper.xml 中创建selectByName方法。注意:用户表中一定要有用户名和用户密码这两个字段。
- LoginUser.java
package com.etc.securitydemo.entity;
import java.io.Serializable;
/**
* null
* @TableName tbl_user
*/
public class LoginUser implements Serializable {
/**
* 序号
*/
private Integer userid;
/**
* 账号
*/
private String username;
/**
* 密码
*/
private String userpwd;
/**
*
*/
private String usersex;
/**
* 电话
*/
private String userphone;
/**
* 0表示异常/1表示正常
*/
private Integer userstate;
/**
* 收货地址
*/
private String address;
private static final long serialVersionUID = 1L;
/**
* 序号
*/
public Integer getUserid() {
return userid;
}
/**
* 序号
*/
public void setUserid(Integer userid) {
this.userid = userid;
}
/**
* 账号
*/
public String getUsername() {
return username;
}
/**
* 账号
*/
public void setUsername(String username) {
this.username = username;
}
/**
* 密码
*/
public String getUserpwd() {
return userpwd;
}
/**
* 密码
*/
public void setUserpwd(String userpwd) {
this.userpwd = userpwd;
}
/**
*/
public String getUsersex() {
return usersex;
}
/**
*/
public void setUsersex(String usersex) {
this.usersex = usersex;
}
/**
* 电话
*/
public String getUserphone() {
return userphone;
}
/**
* 电话
*/
public void setUserphone(String userphone) {
this.userphone = userphone;
}
/**
* 0表示异常/1表示正常
*/
public Integer getUserstate() {
return userstate;
}
/**
* 0表示异常/1表示正常
*/
public void setUserstate(Integer userstate) {
this.userstate = userstate;
}
/**
* 收货地址
*/
public String getAddress() {
return address;
}
/**
* 收货地址
*/
public void setAddress(String address) {
this.address = address;
}
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass() != that.getClass()) {
return false;
}
LoginUser other = (LoginUser) that;
return (this.getUserid() == null ? other.getUserid() == null : this.getUserid().equals(other.getUserid()))
&& (this.getUsername() == null ? other.getUsername() == null : this.getUsername().equals(other.getUsername()))
&& (this.getUserpwd() == null ? other.getUserpwd() == null : this.getUserpwd().equals(other.getUserpwd()))
&& (this.getUsersex() == null ? other.getUsersex() == null : this.getUsersex().equals(other.getUsersex()))
&& (this.getUserphone() == null ? other.getUserphone() == null : this.getUserphone().equals(other.getUserphone()))
&& (this.getUserstate() == null ? other.getUserstate() == null : this.getUserstate().equals(other.getUserstate()))
&& (this.getAddress() == null ? other.getAddress() == null : this.getAddress().equals(other.getAddress()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getUserid() == null) ? 0 : getUserid().hashCode());
result = prime * result + ((getUsername() == null) ? 0 : getUsername().hashCode());
result = prime * result + ((getUserpwd() == null) ? 0 : getUserpwd().hashCode());
result = prime * result + ((getUsersex() == null) ? 0 : getUsersex().hashCode());
result = prime * result + ((getUserphone() == null) ? 0 : getUserphone().hashCode());
result = prime * result + ((getUserstate() == null) ? 0 : getUserstate().hashCode());
result = prime * result + ((getAddress() == null) ? 0 : getAddress().hashCode());
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", userid=").append(userid);
sb.append(", username=").append(username);
sb.append(", userpwd=").append(userpwd);
sb.append(", usersex=").append(usersex);
sb.append(", userphone=").append(userphone);
sb.append(", userstate=").append(userstate);
sb.append(", address=").append(address);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
}
- LoginUserMapper.java
package com.etc.securitydemo.mapper;
import com.etc.securitydemo.entity.LoginUser;
/**
* @Entity com.etc.securitydemo.entity.LoginUser
*/
public interface LoginUserMapper {
int deleteByPrimaryKey(Long id);
int insert(LoginUser record);
int insertSelective(LoginUser record);
LoginUser selectByPrimaryKey(Long id);
int updateByPrimaryKeySelective(LoginUser record);
int updateByPrimaryKey(LoginUser record);
LoginUser selectByName(String username);
}
- LoginUserMapper.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.etc.securitydemo.mapper.LoginUserMapper">
<resultMap id="BaseResultMap" type="com.etc.securitydemo.entity.LoginUser">
<id property="userid" column="userid" jdbcType="INTEGER"/>
<result property="username" column="username" jdbcType="VARCHAR"/>
<result property="userpwd" column="userpwd" jdbcType="VARCHAR"/>
<result property="usersex" column="usersex" jdbcType="CHAR"/>
<result property="userphone" column="userphone" jdbcType="VARCHAR"/>
<result property="userstate" column="userstate" jdbcType="INTEGER"/>
<result property="address" column="address" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
userid,username,userpwd,
usersex,userphone,userstate,
address
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from tbl_user
where userid = #{
userid,jdbcType=INTEGER}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
delete from tbl_user
where userid = #{
userid,jdbcType=INTEGER}
</delete>
<insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.etc.securitydemo.entity.LoginUser" useGeneratedKeys="true">
insert into tbl_user
( userid,username,userpwd
,usersex,userphone,userstate
,address)
values (#{
userid,jdbcType=INTEGER},#{
username,jdbcType=VARCHAR},#{
userpwd,jdbcType=VARCHAR}
,#{
usersex,jdbcType=CHAR},#{
userphone,jdbcType=VARCHAR},#{
userstate,jdbcType=INTEGER}
,#{
address,jdbcType=VARCHAR}))
</insert>
<insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.etc.securitydemo.entity.LoginUser" useGeneratedKeys="true">
insert into tbl_user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userid != null">userid,</if>
<if test="username != null">username,</if>
<if test="userpwd != null">userpwd,</if>
<if test="usersex != null">usersex,</if>
<if test="userphone != null">userphone,</if>
<if test="userstate != null">userstate,</if>
<if test="address != null">address,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userid != null">userid = #{
userid,jdbcType=INTEGER},</if>
<if test="username != null">username = #{
username,jdbcType=VARCHAR},</if>
<if test="userpwd != null">userpwd = #{
userpwd,jdbcType=VARCHAR},</if>
<if test="usersex != null">usersex = #{
usersex,jdbcType=CHAR},</if>
<if test="userphone != null">userphone = #{
userphone,jdbcType=VARCHAR},</if>
<if test="userstate != null">userstate = #{
userstate,jdbcType=INTEGER},</if>
<if test="address != null">address = #{
address,jdbcType=VARCHAR},</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="com.etc.securitydemo.entity.LoginUser">
update tbl_user
<set>
<if test="username != null">
username = #{
username,jdbcType=VARCHAR},
</if>
<if test="userpwd != null">
userpwd = #{
userpwd,jdbcType=VARCHAR},
</if>
<if test="usersex != null">
usersex = #{
usersex,jdbcType=CHAR},
</if>
<if test="userphone != null">
userphone = #{
userphone,jdbcType=VARCHAR},
</if>
<if test="userstate != null">
userstate = #{
userstate,jdbcType=INTEGER},
</if>
<if test="address != null">
address = #{
address,jdbcType=VARCHAR},
</if>
</set>
where userid = #{
userid,jdbcType=INTEGER}
</update>
<update id="updateByPrimaryKey" parameterType="com.etc.securitydemo.entity.LoginUser">
update tbl_user
set
username = #{
username,jdbcType=VARCHAR},
userpwd = #{
userpwd,jdbcType=VARCHAR},
usersex = #{
usersex,jdbcType=CHAR},
userphone = #{
userphone,jdbcType=VARCHAR},
userstate = #{
userstate,jdbcType=INTEGER},
address = #{
address,jdbcType=VARCHAR}
where userid = #{
userid,jdbcType=INTEGER}
</update>
<select id="selectByName" parameterType="java.lang.String" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from tbl_user
where username = #{
username,jdbcType=VARCHAR}
</select>
</mapper>
(7)在 src\test\java\com\etc\securitydemo 目录下创建测试类来测试Mapper类。输入encode变量是使用UUID加密后的字符串。
package com.etc.securitydemo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@SpringBootTest
class SecuritydemoApplicationTests {
@Test
void contextLoads() {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123456");
System.out.println(encode);
boolean matches = passwordEncoder.matches("123456", encode);
System.out.println(matches);
}
}
(8)创建config包,并在该包下创建SecurityConfig类。(重要)
package com.etc.securitydemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
(9)创建service包,并在该包下创建UserDetailServiceImpl实现类。(重要)
package com.etc.securitydemo.service;
import com.etc.securitydemo.entity.LoginUser;
import com.etc.securitydemo.mapper.LoginUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
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;
@Service("userDetailServiceImpl")
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private LoginUserMapper loginUserMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
LoginUser loginUser = loginUserMapper.selectByName(s);
if(loginUser == null){
throw new UsernameNotFoundException("用户不存在!");
}
User user = new User(loginUser.getUsername(),loginUser.getUserpwd(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
return user;
}
}
(10)创建controller包,并在该包下创建Controller类。
package com.etc.securitydemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloController {
@GetMapping("hello")
@ResponseBody
public String GetHello(){
return "Hello,Spring Security!";
}
}
(11)在SecuritydemoApplication运行类前添加@MapperScan注解。
package com.etc.securitydemo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.etc.securitydemo.mapper")
public class SecuritydemoApplication {
public static void main(String[] args) {
SpringApplication.run(SecuritydemoApplication.class, args);
}
}
(12)运行SecuritydemoApplication启动类。访问网页 127.0.0.1:8081/hello
,输入数据库中用户表的用户称和密码进行登录。注意:数据库用户表中的密码存储的是使用UUID加密后的密码。