文章目录

  • 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
  1. 关闭csrf跨域防护 (不推荐)
    http.csrf().disable();
  2. 以 form 表单的形式请求 /logout 接口
<form th:action="@{/logout}" method="post">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
    <input type="submit" value="logout">
</form>
  1. 在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--访问数据

Spring Boot Actuator 的安全配置可以关闭么 spring boot安全框架_spring boot

  • 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(), "");
}
实现角色授权
  • 授权方式:
  1. 编程式,通过if / else在controller层实现
if(subject.hasRole("admin")) {
    //有权限
} else {
    //无权限
}
  1. 注解式,在请求方法上加上注解实现 (推荐)
@RequiresPermissions("user:add")
public String add(){
    //有权限
}
  1. 标签式,通过模板语言在页面标签实现,有权限显示标签,否则不显示
  2. 直接在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>