springboot-6-springSecurity

一、Spring Security的基本配置

安全需要在设计网站之初就需要做好设计

可以做到:

  • 功能权限
  • 访问权限
  • 菜单权限

这些权限虽然用拦截器过滤器也能实现,但是很麻烦,所以我们一般使用框架实现

几个重要类:

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

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

  • “认证”Authentication

  • “授权”Authorization

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

流程:

导入依赖,是spring系列的框架

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

写控制器

@RestController
public class Hello {

    @GetMapping("/hello")
    public String getHello(){
        return "hello security";
    }
}

访问:http://localhost:8080/hello

用户名是user

密码为:

登录进去之后才进入了hello的放回界面

配置文件指定用户名和密码:

application.properties

spring.security.user.name=admin
spring.security.user.password=123456
spring.security.user.roles=admin

配置类实现更多的配置:

这是一种基于内存的配置,通过继承WebSecurityConfigurerAdapter

要与数据库认证区分开来,配置角色时不需要再添加“ROLE_”前缀

1、指定用户名和密码和职责(认证)

1、添加配置类

2、继承WebSecurityConfigurerAdapter

3、重写configure(AuthenticationManagerBuilder auth)方法

4、添加加密方式

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

    //springboot要求必须使用一种加密方式
    //此处使用NoOpPasswordEncoder,即无密码加密方式
    @Bean
    PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //配置两个用户
        auth.inMemoryAuthentication()
                .withUser("admin").password("123456").roles("ADMIN","USER")
                .and()
                .withUser("wang").password("123").roles("USER");
    }
}

2、配置受保护资源(授权)

重写的是configure(HttpSecurity http)方法

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //添加了三个成员,分别是数据库管理员、管理员、和用户
    auth.inMemoryAuthentication()
        .withUser("root").password("123456").roles("ADMIN","DBA")
        .and()
        .withUser("admin").password("123456").roles("ADMIN")
        .and()
        .withUser("wang").password("123456").roles("USER");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        //管理员--->管理员角色
        .antMatchers("/admin/**").hasRole("ADMIN")
        //用户--->用户角色或者管理员角色有其一即可
        .antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')")
        //数据库--->管理员角色和数据库管理员角色都要有才能访问
        .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
        //除去上面的请求模式需要特定的角色之外
        // 其他请求都需要先经过认证,才能访问
        .anyRequest().authenticated()
        .and()
        /*表单登录
                * 登录url为"/login",注意这个“/”
                * permitall表示所有与登录相关的接口都不需要认证就可以访问
                * */
        .formLogin().loginProcessingUrl("/login").permitAll()
        .and()
        //表示关闭csrf
        .csrf().disable();
}

添加控制器--测试

@RestController
public class Hello {

    //管理员
    @GetMapping("/admin/hello")
    public String admin(){
        return "hello admin";
    }

    //用户
    @GetMapping("/user/hello")
    public String user(){
        return "hello user";
    }

    //数据库管理员
    @GetMapping("/db/hello")
    public String db(){
        return "hello dba";
    }

    //普通的hello
    @GetMapping("/hello")
    public String Hello(){
        return "hello";
    }
}

会发现只要登录了就可以访问/hello,还有这个用户对应的hello页面,其他以此类推

登录表单详细配置:

就是跟改上面授权中的.formLogin().loginProcessingUrl("/login").permitAll()这一段配置

//表单提交数据
.formLogin()
//下面这个是登录界面,此处没有写这个界面,所以用postman测试
.loginPage("/login_page")
//下面三行值得是提交信息给哪个url,而且用的是哪个参数名称
.loginProcessingUrl("/login")
.usernameParameter("name")
.passwordParameter("password")
//在这个成功操作的方法里面放一个实体静态类
.successHandler(new AuthenticationSuccessHandler() {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        //从认证成功那里得到关键信息
        Object principal = authentication.getPrincipal();
        PrintWriter out = response.getWriter();
        response.setStatus(200);
        //这个map就是我们要放回的json信息
        HashMap<String, Object> map = new HashMap<>();
        map.put("status",200);
        map.put("msg",principal);

        ObjectMapper om = new ObjectMapper();
        out.write(om.writeValueAsString(map));
        out.flush();
        out.close();

    }
})
.failureHandler(new AuthenticationFailureHandler() {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        response.setStatus(401);
        HashMap<String, Object> map = new HashMap<>();
        map.put("status",401);
        if (exception instanceof LockedException){
            map.put("msg","账户被锁,登陆失败");
        }else if(exception instanceof BadCredentialsException){
            map.put("msg","账户名或密码输入错误,登录失败");
        }else if(exception instanceof DisabledException){
            map.put("msg","账户被禁用,登陆失败");
        }else if(exception instanceof CredentialsExpiredException){
            map.put("msg","密码已过期,登陆失败");
        }else if(exception instanceof AccountExpiredException){
            map.put("msg","账户已过期,登陆失败");
        }else{
            map.put("msg","登陆失败");
        }

        ObjectMapper om = new ObjectMapper();
        out.write(om.writeValueAsString(map));
        out.flush();
        out.close();

    }
})
.permitAll()

此处主要是返回一段json数据,数据提交给/login接口

所以可以使用postman测试

注销登录操作:

注销登录,就是类似于qq下线一样

在后面加上一个新的板块

.and()
//开启注销登录的配置
.logout()
//配置注销登录提交星系交给哪个接口
.logoutUrl("/logout")
.clearAuthentication(true)
.invalidateHttpSession(true)
.addLogoutHandler(new LogoutHandler() {
    @Override
    public void logout(HttpServletRequest request,
                       HttpServletResponse response,
                       Authentication authentication) {
        //这个地方根据需要完成一些数据清除工作,如cookie
    }
})
.logoutSuccessHandler(new LogoutSuccessHandler() {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.sendRedirect("/login_page");
    }
})

多个Http Security同时配置:

略(见书)

二、密码加密

三、基于数据库认证

流程:

先创建好表

user、role、user_role三个表

![image-20210707125857051](https://img2020.cnblogs.com/blog/2309666/202107/2309666-20210715104156259-1119834196.png)
![image-20210707125857051](https://img2020.cnblogs.com/blog/2309666/202107/2309666-20210715104156259-1119834196.png)

填充数据

![image-20210707131719146](https://img2020.cnblogs.com/blog/2309666/202107/2309666-20210715104156482-81242353.png)

需要再整合mybatis

导入依赖

<!--mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!--druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.21</version>
</dependency>

配置数据库

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

创建对应pojo类,Role类和User类

@Data
public class Role {
    private Integer id;
    private String name;
    private String nameZh;
}
public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private boolean enabled;
    private boolean locked;
    private List<Role> roles;

    public User() {
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public boolean isLocked() {
        return locked;
    }

    public void setLocked(boolean locked) {
        this.locked = locked;
    }

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }
    /*======================下面为重写方法======================*/

    //这个方法用于获取当前用户所具有的角色对象
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role:roles){
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    //因为数据库中没有创建过期未过期的字段,故直接返回true
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

创建UserMapper接口和UserMapper.xml

@Mapper
@Repository
public interface UserMapper {
    //通过用户名去数据库找这个用户,如果没找到就返回一个账户不存在的错误
    //如果找到了用户,就继续查找该用户对应的角色信息,并将获取到的用户信息返回,系统会自动去比对密码
    User loadUseByUsername(String username);

    //通过用户id来找到这个其对应的角色
    List<Role> getUserRolesByUid(Integer id);
}
<?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">
<mapper namespace="com.wang.dao.UserMapper">
    <!--User loadUseByUsername(String username);-->
    <select id="loadUseByUsername" parameterType="string" resultType="user">
        select * from user where username = # {username}
    </select>

    <!--List<Role> getUserRolesByUid(Integer id);-->
    <select id="getUserRolesByUid" parameterType="integer" resultType="role">
        select * from role r , user_role ur  where r.id = ur.rid and ur.uid = #{id}
    </select>
</mapper>

创建UserService

@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserMapper userMapper;

    //参数即为表单输入的参数
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUseByUsername(username);
        if (user == null){
            throw new UsernameNotFoundException("账户不存在");
        }
        user.setRoles(userMapper.getUserRolesByUid(user.getId()));
        return user;
    }
}

配置Spring Security

User类怎么写

User类必须要实现UserDetails,里面有七个方法需要重写,

方法 解释
getAuthorities() 获取当前用户对象所具有的角色信息
getUsername() 获取当前用户名
isCredentialsNonExpired() 当前用户是否过期(字段)
isAccountNonLocked() 当前用户是否锁定(字段)
isEnabled() 当前账户是否可用(字段)
getPassword() 获取当前用户密码
isCredentialsNonExpired() 当前密码是否过期(密码字段)

getAuthorities()如何获取到角色信息的:

在User类中用户所具有的角色储存在了roles属性中,所以先创建一个SimpleGrantedAuthority的集合,

再遍历每个role,用他们的name创建一个个SimpleGrantedAuthority实体类,放到集合中去,最后放回这个集合即可。

UserService类怎么写的

必须要实现UserDetailsService类,并重写里面的loadUserByUsername方法,这个方法可以直接获取到输入的username,把这个username通过usermapper去查找

如果没找到,我们需要自定自己的输出错误

如果找到了,系统会自动会匹配他的密码,根据相关数据,返回不同的错误

在然后我们通过这个user的id通过usermapper的另一个方法找到他的角色,再把这个角色又赋值给这个user,再返回这个user实例,实现了通过username加载user

一个完整的基于内存的配置

package com.wang.config;


import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.security.auth.login.CredentialException;
import javax.security.auth.login.CredentialExpiredException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;

//@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

    //把这个实例放到IOC容器中去是为了使密码无加密
    @Bean
    PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //添加了三个成员,分别是数据库管理员、管理员、和用户
        auth.inMemoryAuthentication()
                .withUser("root").password("123456").roles("ADMIN","DBA")
                .and()
                .withUser("admin").password("123456").roles("ADMIN")
                .and()
                .withUser("wang").password("123456").roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //管理员--->管理员角色
                .antMatchers("/admin/**").hasRole("ADMIN")
                //用户--->用户角色或者管理员角色有其一即可
                .antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')")
                //数据库--->管理员角色和数据库管理员角色都要有才能访问
                .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
                //除去上面的请求模式需要特定的角色之外
                // 其他请求都需要先经过认证,才能访问
                .anyRequest().authenticated()
                .and()

                //表单提交数据
                .formLogin()
                //下面这个是登录界面,此处没有写这个界面,所以用postman测试
                .loginPage("/login_page")
                //下面三行值得是提交信息给哪个url,而且用的是哪个参数名称
                .loginProcessingUrl("/login")
                .usernameParameter("name")
                .passwordParameter("password")
                //在这个成功操作的方法里面放一个实体静态类
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request,
                                                        HttpServletResponse response,
                                                        Authentication authentication) throws IOException, ServletException {
                        //从认证成功那里得到关键信息
                        Object principal = authentication.getPrincipal();
                        PrintWriter out = response.getWriter();
                        response.setStatus(200);
                        //这个map就是我们要放回的json信息
                        HashMap<String, Object> map = new HashMap<>();
                        map.put("status",200);
                        map.put("msg",principal);

                        ObjectMapper om = new ObjectMapper();
                        out.write(om.writeValueAsString(map));
                        out.flush();
                        out.close();

                    }
                })
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest request,
                                                        HttpServletResponse response,
                                                        AuthenticationException exception) throws IOException, ServletException {
                        response.setContentType("application/json;charset=utf-8");
                        PrintWriter out = response.getWriter();
                        response.setStatus(401);
                        HashMap<String, Object> map = new HashMap<>();
                        map.put("status",401);
                        if (exception instanceof LockedException){
                            map.put("msg","账户被锁,登陆失败");
                        }else if(exception instanceof BadCredentialsException){
                            map.put("msg","账户名或密码输入错误,登录失败");
                        }else if(exception instanceof DisabledException){
                            map.put("msg","账户被禁用,登陆失败");
                        }else if(exception instanceof CredentialsExpiredException){
                            map.put("msg","密码已过期,登陆失败");
                        }else if(exception instanceof AccountExpiredException){
                            map.put("msg","账户已过期,登陆失败");
                        }else{
                            map.put("msg","登陆失败");
                        }

                        ObjectMapper om = new ObjectMapper();
                        out.write(om.writeValueAsString(map));
                        out.flush();
                        out.close();

                    }
                })
                .permitAll()

                .and()
                //开启注销登录的配置
                .logout()
                //配置注销登录提交星系交给哪个接口
                .logoutUrl("/logout")
                .clearAuthentication(true)
                .invalidateHttpSession(true)
                .addLogoutHandler(new LogoutHandler() {
                    @Override
                    public void logout(HttpServletRequest request,
                                       HttpServletResponse response,
                                       Authentication authentication) {
                        //这个地方根据需要完成一些数据清除工作,如cookie
                    }
                })
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        response.sendRedirect("/login_page");
                    }
                })




                .and()
                .csrf()
                .disable();
    }
}

一个完整的基于数据库的配置

@Configuration
public class MyConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserService userService;

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //配置角色的方法
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/db/**").hasRole("dba")
                .antMatchers("/user/**").hasRole("user")
                .anyRequest().authenticated()
                .and()
                .formLogin().loginProcessingUrl("/login").permitAll()
                .and()
                .csrf().disable();
    }
}

角色继承

1、基于security框架的角色继承

在配置类中添加一个bean到IOC容器中去

这个配置保证了dba的权限包含了admin的,admin又包含了dba的

@Bean
RoleHierarchy roleHierarchy(){
    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
    String hierarchy = "ROLE_dba > ROLE_admin > ROLE_user";
    roleHierarchy.setHierarchy(hierarchy);
    return roleHierarchy;
}

2、动态配置权限

使用httpSecurity配置的认证授权规则还是不够灵活,无法实现资源和角色的动态调整,需要开发者自定义权限配置

(1)、数据库设计

在原来的数据库的基础上再增加资源表和资源角色表

(2)添加配置组件类

四、整合shiro

流程:

添加依赖

<!--shiro-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.4.0</version>
</dependency>
<!--整合thymeleaf技术-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <!--不需要版本号-->
</dependency>
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

配置基本信息

# shiro配置
# 开启shiro配置
shiro.enabled=true
# 开启shiroweb配置
shiro.web.enabled=true
# 表示登录地址,默认为/login.jsp
shiro.loginUrl=/login
# 登陆成功地址,默认为/
shiro.successUrl=/index
# 表示当登录之后到未授权界面默认跳转地址
shiro.unauthorizedUrl=/unauthorized
# 表示是否允许通过url参数实现会话跟踪,默认为true(如果支持cookie的话一般设为false)
shiro.sessionManager.sessionIdUrlRewritingEnabled=true
# 表示是否允许通过cookie实现会话跟踪,默认为true
shiro.sessionManager.sessionIdCookieEnabled=true

配置shiro

package com.wang.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.realm.text.TextConfigurationRealm;

import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {

    //注意这个地方的realm对应的包时shiro下的包
    @Bean
    public Realm realm(){
        TextConfigurationRealm realm = new TextConfigurationRealm();
        realm.setUserDefinitions("sang=123,user\n admin=123,admin");
        realm.setRoleDefinitions("admin=read,write\n user=read");
        return realm;
    }

    @Bean
    public ShiroFilterChainDefinition shiroFilterFactoryBean(){
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        //anon表示匿名,logout表示注销操作,authc表示需要登录操作
        chainDefinition.addPathDefinition("/login","anon");
        chainDefinition.addPathDefinition("/dologin","anon");
        chainDefinition.addPathDefinition("/logout","logout");
        chainDefinition.addPathDefinition("/**","authc");
        return chainDefinition;
    }

    //如果在thymeleaf中不使用shiro标签的话,不需要这个bean
    @Bean
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }
}

对应的control

package com.wang.control;


import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class LoginControl {

    //dologin为登录接口
    @PostMapping("doLogin")
    public String doLogin(String username, String password, Model model){
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);
        Subject subject = SecurityUtils.getSubject();
        /* 尝试登录
        * 登录失败:带一个错误信息去登录界面
        * 登录成功:重定向到/index
        * */
        try{
            subject.login(token);
        }catch (AuthenticationException e){
            model.addAttribute("error","用户名或密码错误");
            return "login";
        }
        //这个重定向其实无所谓,因为在配置文件中已经配置了
        return "redirect:/index";
    }

    @RequiresRoles("admin")
    @GetMapping("/admin")
    public String getAdmin(){
        return "admin";
    }

    @RequiresRoles(value = {"admin","user"},logical = Logical.OR)
    @GetMapping("/user")
    public String getUser(){
        return "user";
    }
}

配置对应的mvc配置

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //这些是不需要角色就可以访问到的接口
        registry.addViewController("/login").setViewName("login");
        registry.addViewController("/index").setViewName("index");
        registry.addViewController("/unauthorized").setViewName("unauthorized");
    }
}

还有五个界面, login.html , index.html , admin.html , user.html , unauthorized.html

<!DOCTYPE html>
<html lang="en" xmlns:th=http://www.thymeleaf.org
      xmlns:sec=http://www.thymeleaf.org/extras/spring-security
      xmlns:shiro=http://www.pollix.at/thymeleaf/shiro>
<head>
    
    <title>Title</title>
</head>
<body>
<form th:action="@{/doLogin}" method="post">
    username: <input type="text" name="username"><br/>
    password: <input type="password" name="password"><br/>
    <div th:text="${error}"></div>
    <input type="submit" value="提交">
</form>
</body>
</html>

index.html

<!DOCTYPE html>
<html lang="en" xmlns:th=http://www.thymeleaf.org
      xmlns:sec=http://www.thymeleaf.org/extras/spring-security
      xmlns:shiro=http://www.pollix.at/thymeleaf/shiro>
<head>
    
    <title>Title</title>
</head>
<body>
    <!--<shiro:principal/>可以查看用户名-->
<h3>hello,<shiro:principal/></h3>
<h4><a th:href="@{/logout}">注销操作</a></h4>
<h4><a shiro:hasRole="admin" th:href="@{/admin}">管理员界面</a></h4>
<h4><a shiro:hasAnyRoles="admin,user" th:href="@{/user}">普通用户界面</a></h4>
</body>
</html>

五、shiro的基于数据库的认证

流程:

需要一个通过id找到密码的userService

@Service
public class UserLoginService {
    @Autowired
    UserLoginMapper userLoginMapper;

    public UserLogin getUserLogin(String account){
        return userLoginMapper.getUserLogin(account);
    }
}

然后再ShiroConfig中自定义自己的Realm即可

@Configuration
public class ShiroConfig {
    //注意这个地方的realm对应的包时shiro下的包
    @Bean
    public Realm realm(){
//        TextConfigurationRealm realm = new TextConfigurationRealm();
//        realm.setUserDefinitions("sang=123,user\n admin=123,admin");
//        realm.setRoleDefinitions("admin=read,write\n user=read");
//        return realm;
        return new AuthRealm();
    }

    @Bean
    public ShiroFilterChainDefinition ShiroFilterFactoryBean(){
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        //anon表示匿名,logout表示注销操作,authc表示需要登录操作
        chainDefinition.addPathDefinition("/login","anon");
        chainDefinition.addPathDefinition("/dologin","anon");
        chainDefinition.addPathDefinition("/logout","logout");
        chainDefinition.addPathDefinition("/**","authc");
        return chainDefinition;
    }

    //如果在thymeleaf中不使用shiro标签的话,不需要这个bean
    @Bean
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }
}
class AuthRealm extends AuthorizingRealm{

    @Autowired
    UserLoginService userLoginService;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //认证
        //从token中获取登录时的账户和密码
        UsernamePasswordToken userToken = (UsernamePasswordToken)token;
        String  account = userToken.getUsername();
        String passwordToken = new String(userToken.getPassword());
        //获取数据库中的密码
        String password = userLoginService.getUserLogin(account).getPassword();
        //如果为空就是账号不存在,如果不相同就是密码错误,但是都抛出AuthenticationException,而不是抛出具体错误原因,免得给破解者提供帮助信息
        if (passwordToken == null || !passwordToken.equals(password)){
            return null;
        }
        //认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm
        return new SimpleAuthenticationInfo(account,password,getName());

    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //授权
        //这个地方也可以配置数据库,进行id和角色的匹配
        return null;
    }
}
posted @ 2021-07-15 10:42  Coder-Wang  阅读(162)  评论(0编辑  收藏  举报