SpringBoot中使用SpringSecurity实现认证和授权(入门)

837 阅读13分钟

简介

SpringSecurity 是Spring项目组中用来提供安全认证(authentication)和授权(authorization)服务的框架。所谓的认证通俗的说就是判断正在操作的用户和密码是否匹配,而授权就是控制用户能做什么操作,也就是能干什么能看到什么。

环境搭建

以idea开发工具为例,模板引擎使用的是thymeleaf
pom.xml

	<properties>
		<java.version>1.8</java.version>
		<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
		<thymeleaf-layout-dialect.version>2.3.0</thymeleaf-layout-dialect.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

添加一些模板文件,这些拷贝即可
KungfuController.java

@Controller
public class KungfuController {
	private final String PREFIX = "pages/";
	/**
	 * 欢迎页
	 * @return
	 */
	@GetMapping("/")
	public String index() {
		return "welcome";
	}
	
	/**
	 * 登陆页
	 * @return
	 */
	@GetMapping("/userlogin")
	public String loginPage() {
		return PREFIX+"login";
	}
	
	
	/**
	 * level1页面映射
	 * @param path
	 * @return
	 */
	@GetMapping("/level1/{path}")
	public String level1(@PathVariable("path")String path) {
		return PREFIX+"level1/"+path;
	}
	
	/**
	 * level2页面映射
	 * @param path
	 * @return
	 */
	@GetMapping("/level2/{path}")
	public String level2(@PathVariable("path")String path) {
		return PREFIX+"level2/"+path;
	}
	
	/**
	 * level3页面映射
	 * @param path
	 * @return
	 */
	@GetMapping("/level3/{path}")
	public String level3(@PathVariable("path")String path) {
		return PREFIX+"level3/"+path;
	}
       /**
	 * 登录失败页
	 * @return
	 */
	@GetMapping("/loginError")
	public String loginError() {
		return "loginError";
	}
}

level1/1.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<a th:href="@{/}">返回</a>
	<h1>罗汉拳</h1>
	<p>罗汉拳站当央,打起来不要慌</p>
</body>
</html>

level1/2.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<a th:href="@{/}">返回</a>
	<h1>武当长拳</h1>
	<p>长一点在长一点</p>
</body>
</html>

level1/3.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<a th:href="@{/}">返回</a>
	<h1>全真剑法</h1>
	<p>全都是真的</p>
</body>
</html>

level2/1.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<a th:href="@{/}">返回</a>
	<h1>太极拳</h1>
	<p>
	       一个西瓜圆又圆 劈它一刀成两半 你一半来 给你你不要 给他他不收 那就不给 把两人撵走 他们不走你走 走啦,一挥手,伤自尊
                  不买西瓜别缠我,缓慢纠缠様 两人缠我赖皮,手慢动作左右挥动 看我厉害,转头缓步拍苍蝇状 拍死了,手抱西瓜状+奥特曼十字手+广播操准备运动的站立
    </p>
</body>
</html>

level2/2.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<a th:href="@{/}">返回</a>
	<h1>七伤拳</h1>
	<p>练这拳的人全都死了</p>
</body>
</html>

level2/3.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<a th:href="@{/}">返回</a>
	<h1>梯云纵</h1>
	<p>踩自己的脚往上跳</p>
</body>
</html>

level3/1.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<a th:href="@{/}">返回</a>
	<h1>葵花宝典</h1>
	<p>欲练神功,挥刀自宫</p>
</body>
</html>

level3/2.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<a th:href="@{/}">返回</a>
	<h1>龟派气功</h1>
	<p>龟-派-气-功-波</p>
</body>
</html>

level3/3.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<a th:href="@{/}">返回</a>
	<h1>独孤九剑</h1>
	<p>欲练此剑,必先犯贱</p>
</body>
</html>

login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h1 align="center">欢迎登陆武林秘籍管理系统</h1>
	<hr>
	<div align="center">
		<form action="" method="post">
			用户名:<input name=""/><br>
			密码:<input name=""><br/>
			<input type="submit" value="登陆">
		</form>
	</div>
</body>
</html>

loginError.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>登录错误</h1>
</body>
</html>

welcome.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1 align="center">欢迎光临武林秘籍管理系统</h1>
<h2 align="center">游客您好,如果想查看武林秘籍 <a th:href="@{/login}">请登录</a></h2>
<hr>

<h3>普通武功秘籍</h3>
<ul>
	<li><a th:href="@{/level1/1}">罗汉拳</a></li>
	<li><a th:href="@{/level1/2}">武当长拳</a></li>
	<li><a th:href="@{/level1/3}">全真剑法</a></li>
</ul>

<h3>高级武功秘籍</h3>
<ul>
	<li><a th:href="@{/level2/1}">太极拳</a></li>
	<li><a th:href="@{/level2/2}">七伤拳</a></li>
	<li><a th:href="@{/level2/3}">梯云纵</a></li>
</ul>

<h3>绝世武功秘籍</h3>
<ul>
	<li><a th:href="@{/level3/1}">葵花宝典</a></li>
	<li><a th:href="@{/level3/2}">龟派气功</a></li>
	<li><a th:href="@{/level3/3}">独孤九剑</a></li>
</ul>

</body>
</html>

项目的目录结构如下:

然后我们可以先启动项目,访问下项目地址,检查下环境是否正常:

这个时候我们可以点击上面的每个功夫链接到每个功夫页面中。

内存中配置用户信息实现权限与授权

首先引入SpringSecurity的依赖


<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

增加一个配置类config/MySecurityConfig.java,需要继承WebSecurityConfigurerAdapter

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Override
    protected void configure(HttpSecurity http) throws Exception {
        //super.configure(http);
        //定制请求的授权规则
        http.authorizeRequests().antMatchers("/").permitAll()//访问/路径不需要拥有任何角色
                .antMatchers("/level1/**").hasRole("VIP1")//访问此路径需要VIP1角色
                .antMatchers("/level2/**").hasRole("VIP2")
                .antMatchers("/level3/**").hasRole("VIP3");
    }
}

这时候我们再重启,访问每个需要角色的路径时就会被拒绝访问

这时候需要我们添加自动登录的功能(在上面的configure(HttpSecurityhttp)方法中添加),会给我们生成一个自动登录到页面,发送指定的请求/login会来到登录页面,如果登录失败会重定向到/login?error表示登录失败

http.formLogin();

如果没有登录访问需要权限的url就会来到这个登录页面。
具体的登录信息我们可以在类中定义,也可以在数据库中定义。
定义认证规则
首先重写configure(AuthenticationManagerBuilder auth)方法

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       //这里定义的是从内存中取出用户名和密码,后面的是数据库中取出来验证
        auth.inMemoryAuthentication()
                .withUser("zhangsan").password("123456").roles("VIP1","VIP2")//分别表示登录名,登录密码,此用户拥有的角色
                .and()
                .withUser("lisi").password("123456").roles("VIP2","VIP3")
                .and()
                .withUser("wangwu").password("123456").roles("VIP1","VIP3");
    }

定义完认证规则之后我们可以重启项目,再次访问,这时候访问需要角色的页面时会重定向到登录页面,这时候我们输入上面定义的任一用户登录信息,登录成功后即可访问对应角色的页面。
接下来我们可以加上注销功能
MySecurityConfig配置类configure(HttpSecurity http)方法中加入自动注销功能

    //注销成功会默认返回/login?logout
    http.logout().logoutSuccessUrl("/");//指定注销成功以后来到首页
    //前台页面post请求访问/logout表示用户注销,并且清空session

前台页面welcome.html加上退出表单

<form th:action="@{/logout}" method="post"><!-- 方法需要时post -->
		<input type="submit" value="注销"/>
	</form>

这时候我们再次重启项目访问项目地址登录之后就可以注销了,注销之后就不能访问任何需要角色的页面了。

接下来再增加认证授权在前台页面的应用
前台页面代码如下

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
	  xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>Insert title here</title>
</head>
<body>
<h1 align="center">欢迎光临武林秘籍管理系统</h1>
<div sec:authorize="!isAuthenticated()"><!-- 如果没认证 -->
	<h2 align="center">游客您好,如果想查看武林秘籍 <a th:href="@{/login}">请登录</a></h2>
</div>
<div sec:authorize="isAuthenticated()"><!-- 如果认证认证了 -->
    <!-- 这里可以读取到登录的用户名和拥有的角色 -->
	<h2><span sec:authentication="name"></span>,您好,您的角色有:
		<span sec:authentication="principal.authorities"></span></h2>
	<form th:action="@{/logout}" method="post"><!-- 方法需要时post -->
		<input type="submit" value="注销"/>
	</form>
</div>
<hr>

<div sec:authorize="hasRole('VIP1')"><!-- 对拥有VIP1角色的用户显示 -->
	<h3>普通武功秘籍</h3>
	<ul>
		<li><a th:href="@{/level1/1}">罗汉拳</a></li>
		<li><a th:href="@{/level1/2}">武当长拳</a></li>
		<li><a th:href="@{/level1/3}">全真剑法</a></li>
	</ul>

</div>

<div sec:authorize="hasRole('VIP2')"><!-- 对拥有VIP2角色的用户显示 -->
	<h3>高级武功秘籍</h3>
	<ul>
		<li><a th:href="@{/level2/1}">太极拳</a></li>
		<li><a th:href="@{/level2/2}">七伤拳</a></li>
		<li><a th:href="@{/level2/3}">梯云纵</a></li>
	</ul>

</div>

<div sec:authorize="hasRole('VIP3')"><!-- 对拥有VIP3角色的用户显示 -->
	<h3>绝世武功秘籍</h3>
	<ul>
		<li><a th:href="@{/level3/1}">葵花宝典</a></li>
		<li><a th:href="@{/level3/2}">龟派气功</a></li>
		<li><a th:href="@{/level3/3}">独孤九剑</a></li>
	</ul>
</div>
</body>
</html>

我们还需要在pom文件中加一个依赖:SpringSecurity和thymeleaf的整合模块

    <properties><!-- 在properties控制下版本 -->
    	<thymeleaf-extras-springsecurity4.version>3.0.2.RELEASE</thymeleaf-extras-springsecurity4.version>
   </properties
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity4</artifactId>
    </dependency>

这时候我们再次访问登录就可以看到页面上所登录用户的不同展示的差异了

记住登录状态
此时我们登录之后关闭浏览器再次启动后,访问项目需要重新登录。如果能够记住登录状态,以后只需要登录一次,只要不注销,重启浏览器访问也依然是登录状态。
开启记住我功能
在配置类的configure(HttpSecurity http)方法中添加

http.rememberMe();

当我们添加上面的配置时,登录页面就会自动加上一个记住的按钮。SpringSecurity是将登录信息存储到cookie中,默认存在的时间为14天。但是如果点击注销,就会立即清除这个cookie信息。
指定自己的登录页面,

  1. 添加login.html的登录请求/userlogin,并绑定input输入框
        <form th:action="@{/userlogin}" method="post">
        用户名:<input name="user"/><br>
        密码:<input name="pwd"><br/>
        <input type="checkbox" name="remeber">记住我 <br/>
        <input type="submit" value="登陆">
    </form>
    
  2. 修改默认登录请求
 http.formLogin().usernameParameter("user").passwordParameter("pwd")//指定登录input标签名称
            .loginPage("/userlogin").failureUrl("/loginError");//指定定制的登录请求和登录失败后的请求
            http.rememberMe().rememberMeParameter("remeber");//添加记住标签的参数名称
  1. 将welcome.html页面的登录请求更换为/userlogin
<a th:href="@{/userlogin}">请登录</a>

这时候访问的时候就会来到自定义的登录页面

数据库用户信息实现权限与授权

创建几张表如下:

CREATE TABLE USER(
	user_id INT PRIMARY KEY AUTO_INCREMENT,
	NAME VARCHAR(30),
	PASSWORD VARCHAR(30)
)
CREATE TABLE role(
	role_id INT PRIMARY KEY AUTO_INCREMENT,
	NAME VARCHAR(30)
)
CREATE TABLE user_role(
	role_id INT,
	user_id INT
)
-- 插入数据  这里使用的是MD5加密算法,密码是123456
INSERT INTO role(NAME)VALUES('ROLE_VIP1');
INSERT INTO role(NAME)VALUES('ROLE_VIP2');
INSERT INTO role(NAME)VALUES('ROLE_VIP3');
INSERT INTO USER(NAME,PASSWORD)VALUES('zhangsan','a3caed36f0fe5a01e5f144db8927235e');
INSERT INTO USER(NAME,PASSWORD)VALUES('list','a3caed36f0fe5a01e5f144db8927235e');
INSERT INTO USER(NAME,PASSWORD)VALUES('wangwu','a3caed36f0fe5a01e5f144db8927235e');
INSERT INTO user_role(role_id,user_id)VALUES(1,1);
INSERT INTO user_role(role_id,user_id)VALUES(2,1);
INSERT INTO user_role(role_id,user_id)VALUES(3,2);
INSERT INTO user_role(role_id,user_id)VALUES(2,2);
INSERT INTO user_role(role_id,user_id)VALUES(3,3);

引入pom文件

<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.4</version>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.1.10</version>
		</dependency>

加入application.properties配置信息

server.port=8085
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456

编写实体类UserInfo

public class UserInfo implements Serializable {
    private Integer userId;
    private String name;
    private String password;
    private List<Role> roles;
    //...
}

编写实体类Role

public class Role implements Serializable {
    private Integer roleId;
    private String name;
    //...
}

编写dao
UserMapper

public interface UserMapper {

    @Select("select * from user where name=#{username}")
    @Results({
            @Result(id = true, property = "userId", column = "user_id"),
            @Result(property = "name", column = "NAME"),
            @Result(property = "password", column = "PASSWORD"),
            @Result(property = "roles",column = "user_id",javaType = java.util.List.class,many = @Many(select = "com.example.mapper.RoleMapper.findRoleByUserId"))
    })
    UserInfo findByUsername(String username);
}

RoleMapper

public interface RoleMapper {

    @Select("select * from role where role_id in (select role_id from user_role where user_id=#{userId})")
    @Results({
            @Result(id = true, property = "roleId", column = "role_id"),
            @Result(property = "name", column = "name")
    })
    Role findRoleByUserId(Integer userId);
}

编写service UserService需要继承UserDetailsService

public interface UserService extends UserDetailsService {//继承UserDetailsService

    UserInfo findByUsername(String username);
}

实现类

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserInfo findByUsername(String username) {
        return userMapper.findByUsername(username);
    }

    /**
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserInfo userInfo = null;

        try {
            userInfo = userMapper.findByUsername(username);
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println(userInfo);
        User user = new User(userInfo.getName(),userInfo.getPassword(),getAuthority(userInfo.getRoles()));
        return user;
    }

    public List<SimpleGrantedAuthority> getAuthority(List<Role> roles) {
        List<SimpleGrantedAuthority> list = new ArrayList<>();
        for (Role role : roles) {
            list.add(new SimpleGrantedAuthority(role.getName()));
        }
        return list;
    }
}

配置类MySecurityConfig中需要修改configure(AuthenticationManagerBuilder auth)方法

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(customUserService())//user Details Service验证
             .passwordEncoder(new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {//指定下加密规则
                return MD5Utils.encode((String) rawPassword);
            }

            @Override//匹配接收到的密码是否和数据库中查询到的一致
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return encodedPassword.equals(MD5Utils.encode((String)rawPassword));
            }
        });
}

最后加密类

public class MD5Utils {

    private static final String SALT = "tamboo";

    public static String encode(String password) {
        password = password + SALT;
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        char[] charArray = password.toCharArray();
        byte[] byteArray = new byte[charArray.length];

        for (int i = 0; i < charArray.length; i++)
            byteArray[i] = (byte) charArray[i];
        byte[] md5Bytes = md5.digest(byteArray);
        StringBuffer hexValue = new StringBuffer();
        for (int i = 0; i < md5Bytes.length; i++) {
            int val = ((int) md5Bytes[i]) & 0xff;
            if (val < 16) {
                hexValue.append("0");
            }

            hexValue.append(Integer.toHexString(val));
        }
        return hexValue.toString();
    }
}

现在可以访问项目地址,登录的密码是123456

第一次写博客,水平有限,思路也有问题,语言组织的也有问题,希望谅解