个人博客:http://blog.kunpw.cn
1.初始化导入资源
-
新建项目并配置SpringWeb、Thymeleaf和SpringSecurity依赖(注:只要配置了SpringSecurity依赖后续资源在访问时就会自动被拦截并跳转到登录页,即使并没有配置登录账号);
-
导入素材,资源链接:https://pan.baidu.com/s/1CsbZrhFKggnucYPHFVaHSw 提取码:v7g7
-
新建Controller目录,下建
RouterController.java
路由控制器,实现简单路由即可:@Controller public class RouterController { @RequestMapping({"/","/index"}) public String index(){ return "index"; } @RequestMapping("/toLogin") public String toLogin(){ return "views/login"; } @RequestMapping("/level1/{id}") public String level1(@PathVariable("id")Integer id){ return "views/level1/"+id; } @RequestMapping("/level2/{id}") public String level2(@PathVariable("id")Integer id){ return "views/level2/"+id; } @RequestMapping("/level3/{id}") public String level3(@PathVariable("id")Integer id){ return "views/level3/"+id; } }
2.SpringSecurity权限及认证
SpringSecurity官方文档这里有简单的Security配置,按照此模式配置自己的config文件:
2.1 权限及认证
-
新建
SecurityConfig.java
代码,实现权限及认证功能: -
这里主要是查看源码
WebSecurityConfigurerAdapter.java
文件中相关重写方法,照本临摹实现一些小功能
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
/* 查看源码中该方法进行类比重写
authorizeRequests()是授权请求
antMatchers()为不同路径设置访问权限
*/
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/*").hasRole("vip1")
.antMatchers("/level2/*").hasRole("vip2")
.antMatchers("/level3/*").hasRole("vip3");
// 没有授权则会默认进入登录页面,查看源码可知自动跳转/login
http.formLogin();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/* inMemoryAuthentication()是从内存中获取认证信息,还有jdbcAuthentication()从数据库中获取认证信息这里暂未连接数据库
从源码示例中拿出.withUser("user").password("password").roles("USER").and()模板使用
但是这种未加密密码传输安全受到威胁,需要使用passwordEncoder()加密,至于加密方式BCryptPasswordEncoder()则看需求
* */
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3").and()
.withUser("zhaoan").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2").and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
}
有关于JDBC数据库连接认证看这里:可能本人后续也会具体讲解
2.2 定制登录页及注销
- 定制登录页以及注销,修改上面的代码即可:
// 没有授权则会默认进入登录页面,查看源码可知自动跳转/login
// 定制跳转登录页,并绑定表单中用户名和密码
http.formLogin().loginPage("/toLogin").usernameParameter("username").passwordParameter("password");
// 开启注销及跳转功能
http.logout().logoutSuccessUrl("/");
// 可能需要开启防跨站get访问,注销时使用
http.csrf().disable();
并且修改自定义login.html
页面表单提交:
<!-- Post请求是Security默认设置的认证,以及之后跳转,而不是RouterController实现 -->
<form th:action="@{/toLogin}" method="post">
2.3 Thymeleaf-SpringSecurity搭配使用注销及用户显示
- 在maven仓库中搜索Thymeleaf,查找相关依赖,maven仓库链接:https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity4/3.0.4.RELEASE
添加依赖:
<!-- thymeleaf-SpringSecurity依赖,注意此版本可能过旧,只能使用SpringBoot2.0.9最高,但本人2.4.3可以正常使用 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
index.html
:初次之外记得上一点中设置的logout相关
且此处使用的ui为:semantic-ui,官网国内引入版:https://zijieke.com/semantic-ui/elements/icon.php
<!--引入sec命名-->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<!--登录注销-->
<div class="right menu">
<!--未登录-->
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/toLogin}">
<i class="sign-in icon"></i> 登录
</a>
</div>
<!--已登录,显示用户名和注销-->
<div sec:authorize="isAuthenticated()">
<a class="item">
用户名:<span sec:authentication="name"></span></a>
</div>
<div sec:authorize="isAuthenticated()">
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注销
</a>
</div>
</div>
2.4 权限分级显示
在每一栏上增加此权限分级认证语句即可
<div class="column" sec:authorize="hasRole('vip3')">
2.5 记住我功能
login.html
:在提交按钮上面增加此复选框
<div class="field">
<input type="checkbox" name="remember">记住我
</div>
SecurityConfig.java
:
// 开启记住我且绑定表单参数
http.rememberMe().rememberMeParameter("remember");
3.Shiro整合使用
3.1 Shiro架构及初始化
- Subject:
- 应用代码直接交互的对象是Subject, 也就是说Shiro的对外API核心就是Subject,Subject代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等,与Subject的所有交互都会委托给SecurityManager;;Subject其实是一个门面,SecurityManageer才是实际的执行者;
- ShiroSecurityManager:
- 安全管理器,即所有与安全有关的操作都会与SercurityManager交互,并且它管理着所有的Subject,可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC的DispatcherServlet的角色;
- Realm:
- Shiro从Realm获取安全数据(如用户,角色,权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较,来确定用户的身份是否合法;也需要从Realm得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以把Realm看成DataSource;
-
新建项目,添加Web和Thymeleaf依赖,以及添加Shiro-Spring依赖:
<!-- 引入shiro-spring依赖 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.7.1</version> </dependency>
-
新建前端资源:
- 在
templates
目录下新建index.html,user/vip1.html、user/vip2.html三个简单文件,只需添加文字表示各页面即可;
- 在
-
新建controller包,下建
MyController.java
文件,简单配置路由:@Controller public class MyController { @RequestMapping({"/","/index"}) public String index(Model model){ model.addAttribute("msg","hello,shiro"); return "index"; } @RequestMapping("/user/vip1") public String vip1(Model model){ model.addAttribute("msg","hello,vip1"); return "user/vip1"; } @RequestMapping("/user/vip2") public String vip2(Model model){ model.addAttribute("msg","hello,vip2"); return "user/vip2"; } }
并对前端html文件设置Thymeleaf模板接收参数显示在页面上,并在首页增加跳转链接到其余两个页面,这个内容和SpringSecurity初始化资源差不多。
-
新建config包,下建
UserRealm.java
文件和ShiroConfig.java
文件:UserRealm.java
:// 自定义 UserRealm extends AuthorizingRealm public class UserRealm extends AuthorizingRealm { // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { return null; } }
ShiroConfig.java
:@Configuration public class ShiroConfig { // 1.创建realm对象,需要自定义类 @Bean public UserRealm userRealm(){ return new UserRealm(); } // 2.DefaultWebSecurityManager @Qualifier("userRealm")表示绑定方法userRealm,有两种方式如下 @Bean(name = "securityManager") public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 关联userRealm securityManager.setRealm(userRealm); return securityManager; } // 3.ShiroFilterFactoryBean @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // 设置安全管理器 bean.setSecurityManager(securityManager); return bean; } }
此时就完成了简单的Shiro配置以及前端页面的链接跳转,向SpringSecurity一样,接下来其实就是登录、权限、注销等功能实现。
3.2 拦截器及跳转登录
-
增加登录页,新建
login.html
,实现简单登录表单:<h1>登录</h1> <hr> <form action=""> <p>用户名:<input type="text" name="username"></p> <p>密码:<input type="text" name="password"></p> <p><input type="submit"></p> </form>
-
设置路由,在
MyController.java
文件中增加,实现路由:@GetMapping("/login") public String login(){ return "login"; }
-
在
ShiroConfig.java
文件中shiroFilterFactoryBean()方法中增加过滤设置以及无权限跳转登录设置// 添加shiro内置过滤器 /* anon 无需认证就可以访问 authc 必须拥有认证才可以访问 user 必须拥有记住我功能才能访问 perms 拥有对某个资源的权限才能访问 role 拥有某个角色权限才能访问 */ Map<String,String> filterMap = new LinkedHashMap<>(); filterMap.put("/user/*","authc"); bean.setFilterChainDefinitionMap(filterMap); // 设置登录请求,即拦截之后跳转页面 bean.setLoginUrl("/login");
3.3 整合Mybatis、druid认证登录
-
添加依赖:
<!-- lombok可自动注入有参无参构造等 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- 引入mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- 引入mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <!-- 引入druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.4</version> </dependency> <!-- 引入log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency>
如四中新建
log4j.properties
文件并初始化:log4j.rootLogger=DEBUG, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
-
在
application.yml
文件(没有该文件在application.properties同级创建该文件)中增加数据库连接druid数据源等配置:spring: datasource: username: root password: password # serverTimezone=UTC是时区, url: jdbc:mysql://localhost:3306/springboot_mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource # Spring默认不注入这些属性配置,需要自己绑定,一般根据公司需要个性绑定,也是druid专有属性 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true # 配置filter,stat:监控统计;log4j:日志记录;wall:防御sql注入 filters: stat,wall,log4j maxPoolPreparedStatmentPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 # 整合mybatis,分别为实体类位置和mapper实现层位置,这里暂未使用,等会用得上 mybatis: type-aliases-package: com.kun.pojo mapper-locations: classpath:mapper/*.xml
-
MyController.java
接收post请求:login.html
:<h1>登录</h1> <hr> <p th:text="${msg}" style="color: red"></p> <form action="/login" method="post"> <p>用户名:<input type="text" name="username"></p> <p>密码:<input type="password" name="password"></p> <p><input type="submit"></p> </form>
MyController.java
增加post请求:// 接收表单数据并封装成token存入subject中,传给UserRealm中认证 @PostMapping("/login") public String loginAuthentication( @PathVariable("username") String username, @PathVariable("password") String password, Model model){ // 获取当前用户 Subject subject = SecurityUtils.getSubject(); // 封装当前用户的登录数据 UsernamePasswordToken token = new UsernamePasswordToken(username,password); try { // 将该用户token传给认证doGetAuthenticationInfo subject.login(token); }catch (UnknownAccountException e){ model.addAttribute("msg","用户名错误"); return "login"; }catch (IncorrectCredentialsException e){ model.addAttribute("msg","密码错误"); return "login"; } return "index"; }
-
新建pojo、mapper和resources下mapper三个包:
pojo.User.java
:@Data @AllArgsConstructor @NoArgsConstructor public class User { private int id; private String name; private String pwd; }
mapper.UserMapper.java
:@Mapper @Repository public interface UserMapper { User getUserByName(String name); }
mapper.UserMapper.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"> <!-- 注意此处需要设置连接位置 这里使用连接的数据库中的user表,表中属性为id,user,pwd--> <mapper namespace="com.kun.mapper.UserMapper"> <select id="getUserByName" parameterType="String" resultType="User"> select * from user where name = #{name} </select> </mapper>
-
再建service包(服务层,具体各层以及常用注解请参看本人博客SpringBoot原理),下建
UserService.java
、UserServiceImpl.java
两个文件:UserService.java
:public interface UserService { User getUserByName(String name); }
UserServiceImpl.java
:@Service public class UserServiceImpl implements UserService{ @Autowired UserMapper userMapper; @Override public User getUserByName(String name) { return userMapper.getUserByName(name); } }
-
UserRealm.java
实现认证即可正常使用数据库用户登录:// 自定义 UserRealm extends AuthorizingRealm public class UserRealm extends AuthorizingRealm { @Autowired UserService userService; // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("执行了=>认证doGetAuthenticationInfo"); // 将传入的认证token转换成Controller中封装的原token UsernamePasswordToken userToken = (UsernamePasswordToken) token; // 连接真实数据库获取数据 User user = userService.getUserByName(userToken.getUsername()); // 没有此人只要返回null即可,会自动将其识别为UnknownAccountException回到 if(user==null){ return null; } // 密码认证,shiro自己做,而不需要自己接触密码,SimpleAuthenticationInfo使用简单认证,没有加密,可以尝试加密 // 查看SimpleAuthenticationInfo源码实现加密传输密码,md5,md5盐值加密 return new SimpleAuthenticationInfo("",user.getPwd(),""); } }
3.4 授权
-
修改user表,增加perms权限一栏,并为部分用户授权:
-
增加pojo下User实体类属性perms:
private String perms;
-
在
MyController.java
文件中增加无权限访问路由:// @ResponseBody 是返回字符串到空白页面 @RequestMapping("/noauth") @ResponseBody public String noauth(){ return "没有权限无法访问"; }
-
在
ShiroConfig.java
文件中设置权限拦截以及无权限时跳转页面:/*filterMap.put("/user/*","authc");*/ // 设置权限拦截 filterMap.put("/user/vip1","perms[user:vip1]"); filterMap.put("/user/vip2","perms[user:vip2]"); // 设置无权限跳转页面 bean.setUnauthorizedUrl("/noauth");
-
在
UserRealm.java
文件认证doGetAuthenticationInfo()方法中修改返回值方法传参:// 第一个参数为principal,将该参数存到subject中,由授权获取 return new SimpleAuthenticationInfo(user,user.getPwd(),"");
对授权doGetAuthorizationInfo()方法作如下操作:
// 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("执行了=>授权doGetAuthorizationInfo"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Subject subject = SecurityUtils.getSubject(); // 获取当前用户Subject的Principals,即为认证最后返回存储的user User currentUser = (User)subject.getPrincipal(); // 为当前info增加当前Subject的权限,set可以设置一个集合的权限 info.addStringPermission(currentUser.getPerms()); return info; }
即可成功授权访问。
3.5 shiro-thymeleaf
-
在maven仓库查询thymeleaf-shiro,添加最新版本进入依赖:
pom.xml
:<!-- thymeleaf-shiro --> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency>
-
在
ShiroConfig.java
文件中增加方法整合二者,并加入bean,没有此方法html中引用shiro不会成功:// 整合shiro-thymeleaf @Bean public ShiroDialect getShiroDialect(){ return new ShiroDialect(); }
-
修改首页index.html:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <h1>首页</h1> <div th:text="${msg}"></div> <!-- 从session中判断值 --> <div th:if="${session.userLogin==null}"> <a th:href="@{/login}">登录</a> </div> <hr> <!-- 使用thymeleaf-shiro获取用户权限 --> <div shiro:hasPermission="user:vip1"> <a th:href="@{/user/vip1}">vip1</a> </div> <div shiro:hasPermission="user:vip2"> <a th:href="@{/user/vip2}">vip2</a> </div> </body> </html>
-
在
MyController.java
文件中loginAuthentication()方法中增加Session:// 接收表单数据并封装成token存入subject中,传给UserRealm中认证 @PostMapping("/login") public String loginAuthentication( String username, String password, Model model){ // 获取当前用户 Subject subject = SecurityUtils.getSubject(); // 封装当前用户的登录数据 UsernamePasswordToken token = new UsernamePasswordToken(username,password); try { // 将该用户token传给认证doGetAuthenticationInfo subject.login(token); }catch (UnknownAccountException e){ model.addAttribute("msg","用户名错误"); return "login"; }catch (IncorrectCredentialsException e){ model.addAttribute("msg","密码错误"); return "login"; } // 对当前subject用户设置session Session session = subject.getSession(); session.setAttribute("userLogin",subject); return "index"; }
取当前用户
Subject subject = SecurityUtils.getSubject();
// 封装当前用户的登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
try {
// 将该用户token传给认证doGetAuthenticationInfo
subject.login(token);
}catch (UnknownAccountException e){
model.addAttribute(“msg”,“用户名错误”);
return “login”;
}catch (IncorrectCredentialsException e){
model.addAttribute(“msg”,“密码错误”);
return “login”;
}
// 对当前subject用户设置session
Session session = subject.getSession();
session.setAttribute(“userLogin”,subject);
return “index”;
}