一、安全简介

在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。

市面上存在比较有名的:Shiro,Spring Security !

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。

Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求。

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

二、SpringBoot集成SpringSecurity

1、实验环境搭建

1、新建一个初始的springboot项目的web模块,thymeleaf模块

  1. <!-- thymeleaf模板 -->
  2. <dependency>
  3. <groupId>org.thymeleaf</groupId>
  4. <artifactId>thymeleaf-spring5</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.thymeleaf.extras</groupId>
  8. <artifactId>thymeleaf-extras-java8time</artifactId>
  9. </dependency>

2、导入静态资源

  1. templates
  2. views
  3. level1
  4. 1.html
  5. 2.html
  6. 3.html
  7. level2
  8. 1.html
  9. 2.html
  10. 3.html
  11. level3
  12. 1.html
  13. 2.html
  14. 3.html
  15. login.html
  16. index.html

3、controller跳转!

  1. @Controller
  2. public class RouterController {
  3. @RequestMapping({"/","index"})
  4. public String index(){
  5. return "index";
  6. }
  7. @RequestMapping("/toLogin")
  8. public String toLogin(){
  9. return "views/login";
  10. }
  11. @RequestMapping("/level1/{id}")
  12. public String level1(@PathVariable("id") int id){
  13. return "views/level1/"+id;
  14. }
  15. @RequestMapping("/level2/{id}")
  16. public String level2(@PathVariable("id") int id){
  17. return "views/level2/"+id;
  18. }
  19. @RequestMapping("/level3/{id}")
  20. public String level3(@PathVariable("id") int id){
  21. return "views/level3/"+id;
  22. }
  23. }

4、测试实验环境是否OK!

2、认识SpringSecurity

Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!

记住几个类:

  • WebSecurityConfigurerAdapter:自定义Security策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnableWebSecurity:开启WebSecurity模式

Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。

“认证”(Authentication)

身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。

身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。

“授权” (Authorization)

授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。

这个概念是通用的,而不是只在Spring Security 中存在。

3、认证和授权

目前,我们的测试环境,是谁都可以访问的,我们使用 Spring Security 增加上认证和授权的功能

1、引入 Spring Security 模块

  1. <!-- security -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-security</artifactId>
  5. </dependency>

2、编写 Spring Security 配置类

查看我们自己项目中的版本,在官网找到对应的帮助文档:

3、编写基础配置类

  1. @EnableWebSecurity // 开启WebSecurity模式
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. // 链式编程
  4. @Override
  5. protected void configure(HttpSecurity http) throws Exception {
  6. // 首页所有人都可以访问, 功能页只有对应有权限的人才能访问
  7. // 请求授权的规则~
  8. http.authorizeRequests()
  9. .antMatchers("/").permitAll()
  10. .antMatchers("/level1/**").hasRole("vip1")
  11. .antMatchers("/level2/**").hasRole("vip2")
  12. .antMatchers("/level3/**").hasRole("vip3");
  13. // 没有权限的默认会到登录页面,需要开启登录的页面
  14. // login
  15. // http.formLogin(); // 跳转到自带的登录页面
  16. http.formLogin().loginPage("/toLogin"); // 跳转到自己定制的登录页面
  17. // 注销,开启了注销功能,跳转首页
  18. // 防止网络工具 : get, post
  19. http.csrf().disable(); // 关闭csrf功能(跨站域请求伪造)
  20. http.logout().logoutSuccessUrl("/");
  21. // 开启记住我功能
  22. // http.rememberMe();
  23. http.rememberMe().rememberMeParameter("remember");
  24. }
  25. // 认证, springboot 2.1.x 可以直接使用
  26. // 密码编码: BCryptPasswordEncoder
  27. // 在Spring Secutiry 5.0+ 新增了很多的加密方法
  28. @Override
  29. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  30. // 在内存用验证用户信息
  31. // 在数据库中验证用户信息 auth.jdbcAuthentication()
  32. auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
  33. .withUser("shuai").password(new BCryptPasswordEncoder().encode("shuai")).roles("vip2","vip3")
  34. .and()
  35. .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
  36. .and()
  37. .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
  38. }
  39. }

4、前端页面,我们需要结合thymeleaf中的一些功能

Maven依赖:

  1. <!-- thymeleaf模板 -->
  2. <dependency>
  3. <groupId>org.thymeleaf</groupId>
  4. <artifactId>thymeleaf-spring5</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.thymeleaf.extras</groupId>
  8. <artifactId>thymeleaf-extras-java8time</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.thymeleaf.extras</groupId>
  12. <artifactId>thymeleaf-extras-springsecurity4</artifactId>
  13. <version>3.0.4.RELEASE</version>
  14. </dependency>

index.html

命名空间

  1. xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"
  1. <div class="ui secondary menu">
  2. <a class="item" th:href="@{/index}">首页</a>
  3. <!--登录注销-->
  4. <div class="right menu">
  5. <!--sec:authorize 需要导入命名空间-->
  6. <!--如果未登录-->
  7. <div sec:authorize="!isAuthenticated()">
  8. <a class="item" th:href="@{/toLogin}">
  9. <i class="address card icon"></i> 登录
  10. </a>
  11. </div>
  12. <!--如果登录,用户名注销-->
  13. <div sec:authorize="isAuthenticated()">
  14. <a class="item">
  15. 用户名:<span sec:authentication="name"></span>
  16. </a>
  17. </div>
  18. <div sec:authorize="isAuthenticated()">
  19. <a class="item" th:href="@{/logout}">
  20. <i class="sign-out icon"></i> 注销
  21. </a>
  22. </div>
  23. </div>
  24. </div>

注意:login.html中的用户名和密码默认是name=”username”和name=”password”,如果写成name或者是pwd的话,需要更改定制登录页面为usernameParameter(“name”),不然会报错。

三、SpringBoot集成shiro

1、认识Shiro

主要功能

三个核心组件:Subject, SecurityManager 和 Realms.

Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。

Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。

SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。

Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。

从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。

Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果系统默认的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。

2、实战

1、依赖

  1. <dependency>
  2. <groupId>com.github.theborakompanioni</groupId>
  3. <artifactId>thymeleaf-extras-shiro</artifactId>
  4. <version>2.0.0</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.projectlombok</groupId>
  8. <artifactId>lombok</artifactId>
  9. </dependency>
  10. <!--
  11. Subject 用户
  12. SecurityManager 管理所有用户
  13. Realm 连接数据
  14. -->
  15. <!--shiro整合spring的包-->
  16. <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
  17. <dependency>
  18. <groupId>org.apache.shiro</groupId>
  19. <artifactId>shiro-spring</artifactId>
  20. <version>1.8.0</version>
  21. </dependency>
  22. <dependency>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-starter-web</artifactId>
  25. </dependency>
  26. <!--Thymeleaf 模板-->
  27. <!-- https://mvnrepository.com/artifact/org.thymeleaf/thymeleaf-spring5 -->
  28. <dependency>
  29. <groupId>org.thymeleaf</groupId>
  30. <artifactId>thymeleaf-spring5</artifactId>
  31. </dependency>
  32. <!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-java8time -->
  33. <dependency>
  34. <groupId>org.thymeleaf.extras</groupId>
  35. <artifactId>thymeleaf-extras-java8time</artifactId>
  36. </dependency>
  37. <!--连接数据库相关-->
  38. <dependency>
  39. <groupId>mysql</groupId>
  40. <artifactId>mysql-connector-java</artifactId>
  41. </dependency>
  42. <!-- 引入 myBatis, 这是MyBatis官方提供的适配 Spring Boot的, 而不是Spring Boot自己的-->
  43. <dependency>
  44. <groupId>org.mybatis.spring.boot</groupId>
  45. <artifactId>mybatis-spring-boot-starter</artifactId>
  46. <version>2.2.2</version>
  47. </dependency>
  48. <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
  49. <dependency>
  50. <groupId>com.alibaba</groupId>
  51. <artifactId>druid</artifactId>
  52. <version>1.2.10</version>
  53. </dependency>
  54. <!--log4j-->
  55. <dependency>
  56. <groupId>log4j</groupId>
  57. <artifactId>log4j</artifactId>
  58. <version>1.2.12</version>
  59. </dependency>

2、编写Shiro的Config

  1. /*
  2. @Qualifier 注解是为了查询Bean的 ,从而使两个方法关联起来
  3. */
  4. @Configuration
  5. public class ShiroConfig {
  6. // ShiroFilterFactoryBean: 3
  7. @Bean
  8. public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
  9. ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
  10. // 设置安全管理器
  11. bean.setSecurityManager(defaultWebSecurityManager);
  12. // 添加shiro的内置过滤器
  13. /*
  14. anno: 无需认证就可以访问
  15. authc: 必须认证了才能访问
  16. user: 必须拥有 记住我 功能才能访问
  17. perms: 拥有对某个资源的权限才能访问
  18. role: 拥有某个角色权限才能访问
  19. */
  20. Map<String, String> filterMap = new LinkedHashMap<>();
  21. filterMap.put("/user/add","perms[user:add]");
  22. filterMap.put("/user/update","perms[user:update]");
  23. filterMap.put("/user/*","authc");
  24. bean.setFilterChainDefinitionMap(filterMap);
  25. // 设置登录的请求
  26. bean.setLoginUrl("/toLogin");
  27. // 未授权页面
  28. bean.setUnauthorizedUrl("/noauth");
  29. return bean;
  30. }
  31. // DefaultWebSecurityManager: 2
  32. @Bean(name = "securityManager")
  33. public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
  34. DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
  35. // 关联UserRealm
  36. securityManager.setRealm(userRealm);
  37. return securityManager;
  38. }
  39. // 创建 Realm 对象 , 需要自定义类: 1
  40. @Bean
  41. public UserRealm userRealm(){
  42. return new UserRealm();
  43. }
  44. // 整合ShiroDialect: 用来整合 shiro thymeleaf
  45. @Bean
  46. public ShiroDialect getShiroDialect(){
  47. return new ShiroDialect();
  48. }
  49. }
  1. // 自定义的 UserRealm extends AuthorizingRealm
  2. public class UserRealm extends AuthorizingRealm {
  3. @Autowired
  4. UserService userService;
  5. // 授权
  6. @Override
  7. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
  8. System.out.println("执行了=>授权doGetAuthorizationInfo");
  9. // SimpleAuthorizationInfo
  10. SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
  11. // info.addStringPermission("user:add");
  12. // 拿到当前登录的这个对象
  13. Subject subject = SecurityUtils.getSubject();
  14. User currentUser = (User) subject.getPrincipal(); // 拿到User对象
  15. // 设置当前用户的权限
  16. info.addStringPermission(currentUser.getPerms());
  17. return info;
  18. }
  19. // 认证
  20. @Override
  21. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  22. System.out.println("执行了=>认证doGetAuthenticationInfo");
  23. // 用户名,密码~ 数据中取
  24. // String name = "shuai";
  25. // String password = "123456";
  26. UsernamePasswordToken userToken = (UsernamePasswordToken) token;
  27. // 连接真实数据库
  28. User user = userService.queryUserByName(userToken.getUsername());
  29. if (user==null){ // 没有这个人
  30. return null; // 抛出异常 UnknownAccountException
  31. }
  32. // 把用户放到Session
  33. Subject currentSubject = SecurityUtils.getSubject();
  34. Session session = currentSubject.getSession();
  35. session.setAttribute("loginUser",user);
  36. // 密码认证,shiro内部做了~ , 加密了
  37. // user存在subject中
  38. return new SimpleAuthenticationInfo(user,user.getPwd(),"");
  39. }
  40. }

3、编写MyController

  1. @Controller
  2. public class MyController {
  3. @RequestMapping({"/","index"})
  4. public String toIndex(Model model){
  5. model.addAttribute("msg","hello shiro");
  6. return "index";
  7. }
  8. @RequestMapping("/user/add")
  9. public String add(){
  10. return "user/add";
  11. }
  12. @RequestMapping("/user/update")
  13. public String update(){
  14. return "user/update";
  15. }
  16. @RequestMapping("/toLogin")
  17. public String toLogin(){
  18. return "login";
  19. }
  20. @RequestMapping("/login")
  21. public String login(String username,String password,Model model){
  22. // 获取当前的用户对象 Subject
  23. Subject subject = SecurityUtils.getSubject();
  24. // 封装用户的登录数据
  25. UsernamePasswordToken token = new UsernamePasswordToken(username, password);
  26. try {
  27. subject.login(token); // 执行登录方法,如果没有异常就说明ok了
  28. return "index";
  29. }catch (UnknownAccountException e){ // 用户名不存在
  30. model.addAttribute("msg","用户名错误");
  31. return "login";
  32. }catch (IncorrectCredentialsException e){ // 密码不正确
  33. model.addAttribute("msg","密码错误");
  34. return "login";
  35. }
  36. }
  37. @RequestMapping("/noauth")
  38. @ResponseBody
  39. public String unauthorized(){
  40. return "未授权页面";
  41. }
  42. }

4、整合Mybatis

application.yaml

  1. spring:
  2. datasource:
  3. username: root
  4. password: shuai
  5. # 假如时区报错了,就增加一个时区的配置就ok了 serverTimezone=UTC
  6. url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
  7. driver-class-name: com.mysql.cj.jdbc.Driver
  8. type: com.alibaba.druid.pool.DruidDataSource
  9. #Spring Boot 默认是不注入这些属性值的,需要自己绑定
  10. #druid 数据源专有配置
  11. initialSize: 5
  12. minIdle: 5
  13. maxActive: 20
  14. maxWait: 60000
  15. timeBetweenEvictionRunsMillis: 60000
  16. minEvictableIdleTimeMillis: 300000
  17. validationQuery: SELECT 1 FROM DUAL
  18. testWhileIdle: true
  19. testOnBorrow: false
  20. testOnReturn: false
  21. poolPreparedStatements: true
  22. #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
  23. #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
  24. #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
  25. filters: stat,wall,log4j
  26. maxPoolPreparedStatementPerConnectionSize: 20
  27. useGlobalDataSourceStat: true
  28. connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

application.properties

  1. # 应用名称
  2. spring.application.name=shiro-springboot
  3. # 应用服务 WEB 访问端口
  4. server.port=8080
  5. # 关闭模板引擎的缓存
  6. spring.thymeleaf.cache=false
  7. # 整合mybatis
  8. # 起别名 User
  9. mybatis.type-aliases-package=com.shuai.pojo
  10. # 映射路径
  11. mybatis.mapper-locations=classpath:mapper/*.xml

5、测试查询用户

在pojo下编写User实体类

在mapper下编写UserMapper接口

  1. @Mapper
  2. @Repository
  3. public interface UserMapper {
  4. User queryUserByName(String name);
  5. }

在resources下创建mapper文件夹下创建UserMapper.xml

  1. <!DOCTYPE mapper
  2. PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  3. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  4. <mapper namespace="com.shuai.mapper.UserMapper">
  5. <select id="queryUserByName" parameterType="String" resultType="User">
  6. select * from user where name = #{name};
  7. </select>
  8. </mapper>

再编写service文件夹下UserService

  1. public interface UserService {
  2. User queryUserByName(String name);
  3. }

UserServiceImpl

  1. @Service
  2. public class UserServiceImpl implements UserService{
  3. @Autowired
  4. UserMapper userMapper;
  5. @Override
  6. public User queryUserByName(String name) {
  7. return userMapper.queryUserByName(name);
  8. }
  9. }

最后再test中测试,看是否能够查询成功!

  1. @SpringBootTest
  2. class ShiroSpringbootApplicationTests {
  3. @Autowired
  4. UserService userService;
  5. @Test
  6. void contextLoads() {
  7. System.out.println(userService.queryUserByName("小帅"));
  8. }
  9. }

6、在index.html中进行权限的测试

命名空间

  1. <!--命名空间的组成就是: thymeleaf的官网地址 + 导入thymeleaf与安全的包-->
  2. xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"
  1. <h1>首页</h1>
  2. <div th:text="${msg}"></div>
  3. <!--从session中判断值!-->
  4. <div th:if="${session.loginUser==null}">
  5. <a th:href="@{/toLogin}">登录</a>
  6. </div>
  7. <hr>
  8. <div shiro:hasPermission="user:add">
  9. <a th:href="@{/user/add}">add</a>
  10. </div>
  11. <div shiro:hasPermission="user:update">
  12. <a th:href="@{/user/update}">update</a>
  13. </div>