文章目录
- SpringSecurity (安全)
- 用户认证和授权
- thymeleaf整合security
- 获取用户名和权限以及是否登录
- 根据用户权限显示不同标签
- 注销页因csrf致404解决方案
- 记住我及首页定制
- Shiro (安全)
- Shiro架构 (外部)
- 整合springboot
- 实现登录拦截
- 整合mybatis
- 实现角色授权
- 整合thymeleaf
SpringSecurity (安全)
- 在web开发中,安全是第一位。过滤器,拦截器
- 功能性需求:否
- 做网站:安全应该在什么时候考虑?设计之初
- shiro和SpringSecurity很像
- 认证,授权,各种权限都是安全来做,如果用过滤器拦截器需要大量的原生代码,产生冗余
- 简介:
- Spring Security是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理
- 记住几个类:
- WebSecurityConfigurerAdapter:自定义Security策略
- AuthenticationManagerBuilder:自定义认证策略
- @EnableWebSecurity:开启WebSecurity模式
用户认证和授权
- 编写Security配置类
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//链式编程
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
//首页所有人可以访问,功能页只有对应有权限的人才能访问
//请求授权的规则
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
//没有权限默认到登录页,自定义登录页以及提交请求
//loginPage是没有权限跳转的页面(设置了该项需要关闭csrf跨域请求,否则注销页404)
//loginProcessingUrl是提交账号密码的请求
//可以自定义表单传参的name属性:.usernameParameter("user").passwordParameter("pwd")
http.formLogin().loginPage("/toLogin").loginProcessingUrl("/login");
//开启注销功能,也可以添加附件功能,如删除cookies,清空session
//http.logout().deleteCookies("remove").invalidateHttpSession(true);
http.logout().logoutUrl("/logout").logoutSuccessUrl("/");
//开启记住我功能,通过添加cookies实现,默认保存两周。自定义接受前端的参数
http.rememberMe().rememberMeParameter("remember");
//关闭csrf跨域防护
http.csrf().disable();
}
//认证
//密码编码:passwordEncoder
//在spring security 5.0+ 中新增了很多的加密方式
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//这些数据正常情况下应该从数据库中读
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("cbc").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2", "vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2", "vip3");
}
}
- 连接数据库获取认证
- 旧版springboot,配置类中配置
@Autowired
private DataSource dataSource;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
UserBuilder users = User.withDefaultPasswordEncoder();
auth
.jdbcAuthentication()
.dataSource(dataSource)
.withDefaultSchema()
.withUser(users.username("user").password("password").roles("USER"))
.withUser(users.username("admin").password("password").roles("USER","ADMIN"));
}
- 新版,直接配置bean
@Bean
UserDetailsManager users(DataSource dataSource) {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
users.createUser(user);
users.createUser(admin);
}
thymeleaf整合security
- 依赖,旧版springboot不支持5版本,使用4版本
<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
- html页面加上命名空间
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
获取用户名和权限以及是否登录
<!--登录注销-->
<div class="right menu">
<!--如果未登录-->
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/toLogin}">
<i class="address card icon"></i> 登录
</a>
</div>
</div>
<!--如果已登录:用户名,注销按钮-->
<div sec:authorize="isAuthenticated()">
<a class="item">
用户名:<span sec:authentication="name"></span>
角色:<span sec:authentication="principal.authorities"></span>
</a>
</div>
<div sec:authorize="isAuthenticated()">
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注销
</a>
</div>
根据用户权限显示不同标签
<div class="content" sec:authorize="hasRole('vip1')">
<h5 class="content">Level 1</h5>
<hr>
<div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
<div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
<div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
</div>
<div class="content" sec:authorize="hasRole('vip2')">
<h5 class="content">Level 2</h5>
<hr>
<div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
<div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
<div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
</div>
注销页因csrf致404解决方案
- 原理:spring security在开启 csrf 防护的情况下,
/logout
必须是以POST方法提交才行,<a>
标签请求是GET
方法,所以报 404
- 关闭csrf跨域防护 (不推荐)
http.csrf().disable();
- 以 form 表单的形式请求
/logout
接口
<form th:action="@{/logout}" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<input type="submit" value="logout">
</form>
- 在spring security的配置中,添加
/logout
能够以GET
请求的配置
http.logout()
.logoutUrl("/logout") //实测这句可以不加
.logoutSuccessUrl("/")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"));
记住我及首页定制
- 配置类中加入
//开启记住我功能,通过添加cookies实现,默认保存两周。自定义接受前端的参数
http.rememberMe().rememberMeParameter("remember");
- 提交表单中加入
<div class="field">
<input type="checkbox" name="remember"> 记住我
</div>
Shiro (安全)
- 简介:
- Apache Shiro是一个Java的安全 (权限) 框架
- Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境
- Shiro可以完成,认证,授权,加密,会话管理,Web集成,缓存等
- 功能:
- Authentication:身份认证、登录,验证用户是不是拥有相应的身份
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户能否进行什么操作,如验证某个用户是否拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个权限
- Session Manager:会话管理,即用户登录后就是第一次会话,在没有退出之前,它的所有信息都在会话中。会话可以是普通的JavaSE环境,也可以是Web环境
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储
- Web Support: Web支持,可以非常容易的集成到Web环境
- Caching:缓存,比如用户登录后,其用户信息,拥有的角色、权限不必每次去查,这样可以提高效率
- Concurrency:Shiro支持多线程应用的并发验证,即,如在一个线程中开启另一个线程,能把权限自动的传播过去
- Testing:提供测试支持
- Run As:允许一个用户假装为另一个用户 (如果他们允许) 的身份进行访问
- Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
Shiro架构 (外部)
- 从外部来看Shiro,即从应用程序角度来观察如何使用shiro完成工作:
Subject--当前用户
Shiro SecurityManager--管理所有Subject
Realm--访问数据
- Subject:任何可以与应用交互的用户
- Security Manager:相当于SpringMVC中的DispatcherServlet,是Shiro的心脏,所有具体的交互都通过Security Manager进行控制,它管理者所有的Subject,且负责进行认证,授权,会话,及缓存的管理
- Authenticator:负责Subject认证,是一个扩展点,可以自定义实现,可以使用认证策略(AuthenticationStrategy),即什么情况下算用户认证通过了
- Authorizer:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作,即控制着用户能访问应用中的那些功能
- Realm:可以有一个或者多个的realm,可以认为是安全实体数据源,即用于获取安全实体的,可以用JDBC实现,也可以是内存实现等等,由用户提供,所以一般在应用中都需要实现自己的realm
- SessionManager:管理Session生命周期的组件,而Shiro并不仅仅可以用在Web环境,也可以用在普通的JavaSE环境中
- CacheManager:缓存控制器,来管理如用户,角色,权限等缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能
- Cryptography:密码模块,Shiro提高了一些常见的加密组件用于密码加密,解密等
整合springboot
- 导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>
实现登录拦截
- 编写Shiro配置文件并实现拦截功能
@Configuration
public class ShiroConfig {
//第三步:ShiroFilterFactoryBean
@Bean //@Bean方法参数会默认被自动装配
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
/*
* anon:无需认证就可访问
* authc:必须认证才能访问
* user:必须拥有 记住我 功能才能用
* perms:拥有对某个资源的权限才能访问
* role:拥有某个角色权限才能访问
* */
//拦截
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/**","authc");
bean.setFilterChainDefinitionMap(filterMap);
//设置登录页
bean.setLoginUrl("/toLogin");
return bean;
}
//第二步:DefaultWebSecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联UserRealm
securityManager.setRealm(userRealm);
return securityManager;
}
//第一步:创建 realm 对象,需要自定义
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
}
- 编写realm类
//自定义的realm
public class UserRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了====>授权");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了====>认证");
//用户名,密码 ---> 数据库中取
String name = "root";
String password = "123456";
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
if (!userToken.getUsername().equals(name)){
return null;//抛出异常 UnknownAccountException
}
//密码认证shiro管理
return new SimpleAuthenticationInfo("",password,"");
}
}
- controller控制
@RequestMapping("/login")
public String login(String username, String password, Model model) {
//获取当前用户
Subject subject = SecurityUtils.getSubject();
//封装用户的登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token); //执行登录方法
return "index";
} catch (UnknownAccountException e) {//用户名不存在
model.addAttribute("msg", "用户名不存在");
return "login";
} catch (IncorrectCredentialsException e) {//密码不存在
model.addAttribute("msg", "密码错误");
return "login";
}
}
整合mybatis
- 编写yaml配置并编写以下文件
- 修改realm类
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了====>认证");
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
//连接数据库
User user = userService.queryUserByName(userToken.getUsername());
if (user == null) {//用户名不存在
return null; //UnknownAccountException
}
//密码认证shiro接管,可以加密:MD5,MD5盐值加密
return new SimpleAuthenticationInfo("", user.getPwd(), "");
}
实现角色授权
- 授权方式:
- 编程式,通过if / else在controller层实现
if(subject.hasRole("admin")) {
//有权限
} else {
//无权限
}
- 注解式,在请求方法上加上注解实现 (推荐)
@RequiresPermissions("user:add")
public String add(){
//有权限
}
- 标签式,通过模板语言在页面标签实现,有权限显示标签,否则不显示
- 直接在ShiroConfig类中加入角色拦截和未授权跳转
//授权拦截,运行访问请求如果是拦截请求的子集则需要放拦截请求前,不然无效
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/update","perms[user:update]");
bean.setFilterChainDefinitionMap(filterMap);
//设置未授权页面,不设置默认错误页面很丑
bean.setUnauthorizedUrl("/noauth");
- 编写realm类授权方法
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了====>授权");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//增加权限
info.addStringPermission("user:add");
return info;
}
- 从数据库获取用户权限
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了====>授权");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//拿到当前登录的这个对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal();//拿到User对象,从认证方法里返回值传参获取
//设置当前用户的权限
info.addStringPermission(currentUser.getPerms());
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了====>认证");
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
//连接数据库
User user = userService.queryUserByName(userToken.getUsername());
if (user == null) {//用户名不存在
return null; //UnknownAccountException
}
//密码认证
return new SimpleAuthenticationInfo(user, user.getPwd(), "");//把User对象进行传参
}
- 一个用户有多个权限
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//拿到当前登录的这个对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal();//拿到User对象,从认证方法里返回值传参获取
List<String> perms = Arrays.asList(currentUser.getPerms().split(","));//将每个权限分隔开并存入集合
//设置当前用户的权限
info.addStringPermissions(perms);
return info;
}
- 关于为什么认证方法中返回
new SimpleAuthenticationInfo(user, user.getPwd(), "");
而授权方法是怎么从subject获取到user对象的:简单来说,认证方法的调用是在登录用户的时候,根据源码可以发现,会进入subject类的实现类DelegatingSubject中的login方法,次方法中Subject subject = securityManager.login(this, token);
该句返回了一个subject对象,而这个方法中其中一步就是获取认证方法的返回值
整合thymeleaf
- 导入依赖
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
- 加入命名空间
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
- 示例:
<div shiro:notAuthenticated><!--未登录时显示-->
<hr>
<a th:href="@{/toLogin}">登录</a>
</div>
<div shiro:hasPermission="user:add"><!--有增加权限时显示-->
<hr>
<a th:href="@{/user/add}">增加</a>
</div>
<div shiro:hasPermission="user:update"><!--有修改权限时显示-->
<hr>
<a th:href="@{/user/update}">修改</a>
</div>
<div shiro:authenticated><!--登录后显示-->
<hr>
<a th:href="@{/logout}">退出</a>
</div>