• SpringBoot+Shiro+Vue实现身份验证


    一、需求

    1、Vue登录加密填写用户名、密码,点击登录之后提交给后端;

    2、后端接收密码,通过shiro实现身份验证;

    3、将登录结果返回给前端。

    二、流程分析

    1、前端将用户名、密码发给后端;

    2、后端接收;

    3、调用 subject.login 方法进行登录;

    4、登录成功之后,生成token,返回给和前端;

    三、Shiro身份验证原理

     

    身份验证,即在应用中谁能证明他就是他本人。一般提供如他们的身份 ID 一些标识信息来表明他就是他本人,如提供身份证,用户名 / 密码来证明。

    在 shiro 中,用户需要提供 principals (身份)和 credentials(证明)给 shiro,从而应用能验证用户身份:

    principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号。

    credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。

    最常见的 principals 和 credentials 组合就是用户名 / 密码了。接下来先进行一个基本的身份认证。

    另外两个相关的概念是之前提到的 Subject 及 Realm,分别是主体及验证主体的数据源。

    四、后端代码实现

    1、JWT和Shiro依赖

    1. <!--shiro和springboot整合包-->
    2. <dependency>
    3. <groupId>org.apache.shiro</groupId>
    4. <artifactId>shiro-spring-boot-starter</artifactId>
    5. <version>1.6.0</version>
    6. </dependency>
    7. <!--jwt-->
    8. <dependency>
    9. <groupId>io.jsonwebtoken</groupId>
    10. <artifactId>jjwt</artifactId>
    11. <version>0.9.1</version>
    12. </dependency>

    2、封装User实体类(本文只写关键字段,数据库根据实际情况设计)

    1. @Data
    2. @AllArgsConstructor
    3. @NoArgsConstructor
    4. public class User {
    5. private Integer id;
    6. // 用户唯一ID
    7. private String userID;
    8. // 用户密码
    9. private String userPassword;
    10. // 密码盐值
    11. private String userSalt;
    12. }

    写一个从前端接收发送过来登陆信息的VO类(可不写,但是为了专业,尽量写)

    1. @Data
    2. @AllArgsConstructor
    3. @NoArgsConstructor
    4. /**
    5. * 接收从前端发过来的数据
    6. */
    7. public class UserVO {
    8. private String userID;
    9. private String userPassword;
    10. }

     写一个登录成功之后,后端往前端发送封装数据的DTO类(可不写,但是为了专业,尽量写)

    1. @Data
    2. @AllArgsConstructor
    3. @NoArgsConstructor
    4. /**
    5. * 这个类主要封装登录成功之后,后台向前端发送的一些数据(可自定义)
    6. */
    7. public class UserDTO {
    8. // 用户ID
    9. private String userID;
    10. // 用户Role
    11. private String userRole;
    12. // 用户Token
    13. private String token;
    14. }

    写一个封装Result,可以给前端返回一些标准数据(自定义)

    1. /**
    2. * controller返回类型
    3. *
    4. * @Return
    5. * code:代码
    6. * data:数据
    7. * msg:消息
    8. */
    9. @Data
    10. @AllArgsConstructor
    11. @NoArgsConstructor
    12. public class Result {
    13. private Integer code;
    14. private Object data;
    15. private String msg;
    16. }

    这里的code可以自定义,放在一个类里面,方便以后调用(可自定义)

    1. /**
    2. * 各操作成功与否代码
    3. */
    4. public class Code {
    5. // 失败
    6. public static final Integer FAILED = 20000;
    7. // 成功
    8. public static final Integer SUCCESS = 20001;
    9. // 错误
    10. public static final Integer ERROR = 20002;
    11. // token失效
    12. public static final Integer TOKEN_INVALID = 40001;
    13. // 无权限
    14. public static final Integer NO_PERMISSION = 40002;
    15. }

    3、关于User实体类的Mapper、Service根据自己所需要的自己定义,下文需要的基本方法有:

    “通过userID获取User对象”。 

    1. public interface UserService {
    2. /**
    3. * 通过用户ID获取User对象
    4. * @param userID
    5. * @return
    6. */
    7. User getUserByUserID(String userID);
    8. }

    4、JWT工具类

             主要包括两个参数和三个方法(可以按需求多写)

            两个参数:token有效期和签名;

            三个方法:创建token、解析token获取内容和验证token是否有效

    1. public class JwtUtil {
    2. // 时间毫秒数,token有效期(毫秒为单位)
    3. private static long time = 1000*60*60*24;
    4. // 签名(随便写,尽量复杂)
    5. private static final String signature = "signature123";
    6. // 创建Token
    7. public static String createToken(String userID, String userRole){
    8. JwtBuilder jwtBuilder = Jwts.builder();//构建JWT对象
    9. String jwtToken = jwtBuilder
    10. // Header
    11. .setHeaderParam("typ","JWT")
    12. .setHeaderParam("alg","HS256")
    13. // payload(可自定义)
    14. .claim("role",userRole)
    15. // 用户ID
    16. .setSubject(userID)
    17. // 设置有效期(毫秒单位)
    18. .setExpiration(new Date(System.currentTimeMillis()+time))
    19. .setId(UUID.randomUUID().toString())
    20. // signature
    21. .signWith(SignatureAlgorithm.HS256, signature)
    22. // compact拼接三部分header、payload、signature
    23. .compact();
    24. return jwtToken;
    25. }
    26. // 通过token获取UserID
    27. public static String getUserIDFromToken(String token){
    28. if(token == null){
    29. return "null";
    30. }
    31. try {
    32. JwtParser jwtParser = Jwts.parser();
    33. Jws claimsJws = jwtParser.setSigningKey(signature).parseClaimsJws(token);
    34. // System.out.println(claimsJws.getBody());
    35. return claimsJws.getBody().getSubject();
    36. } catch (Exception e) {//抛异常,说明token失效
    37. return "null";
    38. }
    39. }
    40. // 验证token是否有效
    41. public static Boolean checkToken(String token){
    42. if(token == null){
    43. return false;
    44. }
    45. try {
    46. JwtParser jwtParser = Jwts.parser();
    47. Jws claimsJws = jwtParser.setSigningKey(signature).parseClaimsJws(token);
    48. return true;
    49. }catch (Exception e){
    50. return false;
    51. }
    52. }
    53. }

    5、Salt工具类,可以随机生成盐,并对密码进行加密

    1. public class SaltUtil {
    2. /**
    3. * 随机生成盐值工具
    4. * @param n
    5. * @return
    6. */
    7. public static String getSalt(int n){
    8. char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray();
    9. StringBuilder stringBuilder = new StringBuilder();
    10. for (int i = 0; i < n; i++) {
    11. char c = chars[new Random().nextInt(chars.length)];
    12. stringBuilder.append(c);
    13. }
    14. return stringBuilder.toString();
    15. }
    16. /**
    17. * 随机生成盐值,对用户密码进行加密
    18. * @param password
    19. * @return
    20. */
    21. public static List encryption(String password){
    22. List msg = new ArrayList<>();
    23. String salt = SaltUtil.getSalt(10);
    24. msg.add(salt);
    25. Md5Hash MD5 = new Md5Hash(password, salt, 1024);
    26. msg.add(MD5.toHex());
    27. return msg;
    28. }
    29. }

    6、定义自己的Realm

    Realm:域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。如我们之前的 ini 配置方式将使用 org.apache.shiro.realm.text.IniRealm

    1. /**
    2. * Shiro自定义Realm
    3. */
    4. public class MyRealm extends AuthorizingRealm {
    5. @Autowired
    6. private UserService userService;
    7. /**
    8. * 授权
    9. * @param principalCollection
    10. * @return
    11. */
    12. @Override
    13. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    14. return null;
    15. }
    16. /**
    17. * 认证
    18. * @param authenticationToken
    19. * @return
    20. * @throws AuthenticationException
    21. *
    22. * 客户端传来的 username 和 password 会自动封装到 token,先根据 username 进行查询,
    23. * 如果返回 null,则表示用户名错误,直接 return null 即可,Shiro 会自动抛出 UnknownAccountException 异常。
    24. * 如果返回不为 null,则表示用户名正确,再验证密码,直接返回 SimpleAuthenticationInfo 对象即可,
    25. * 如果密码验证成功,Shiro 认证通过,否则返回 IncorrectCredentialsException 异常。
    26. */
    27. @Override
    28. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    29. // System.out.println("启动认证");
    30. UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
    31. // 获取存放到数据库中的实体类
    32. User user = userService.getUserIDFromToken(token.getUserId());
    33. // System.out.println(user);
    34. if(user != null){
    35. // 参数列表(实体信息,密码,盐值,realm名称)
    36. return new SimpleAuthenticationInfo(user,user.getPassword(), ByteSource.Util.bytes(user.getSalt()),getName());
    37. }
    38. return null;
    39. }
    40. }

    7、配置ShiroConfig

    1. /**
    2. * Shiro配置类
    3. */
    4. @Configuration
    5. public class ShiroConfig {
    6. /*
    7. * 倒序配置
    8. * 1、先自定义过滤器 MyRealm
    9. * 2、创建第二个DefaultWebSecurityManager,将MyRealm注入
    10. * 3、装配第三个ShiroFilterFactoryBean,将DefaultWebSecurityManager注入,并注入认证及授权规则
    11. * */
    12. //3、装配ShiroFilterFactoryBean,并将 DefaultWebSecurityManager 注入到 ShiroFilterFactoryBean 中
    13. @Bean
    14. public ShiroFilterFactoryBean factoryBean(DefaultWebSecurityManager manager){
    15. ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
    16. factoryBean.setSecurityManager(manager);//将 DefaultWebSecurityManager 注入到 ShiroFilterFactoryBean 中
    17. //注入认证及授权规则
    18. return factoryBean;
    19. }
    20. //2、创建DefaultWebSecurityManager ,并且将 MyRealm 注入到 DefaultWebSecurityManager bean 中
    21. @Bean
    22. public DefaultWebSecurityManager manager(MyRealm myRealm){
    23. DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
    24. manager.setRealm(myRealm);//将自定义的 MyRealm 注入到 DefaultWebSecurityManager bean 中
    25. return manager;
    26. }
    27. //1、自定义过滤器Realm
    28. @Bean
    29. public MyRealm myRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher){
    30. MyRealm myRealm = new MyRealm();
    31. // 密码匹配器
    32. myRealm.setCredentialsMatcher(matcher);
    33. return myRealm;
    34. }
    35. /**
    36. * 密码匹配器
    37. * @return HashedCredentialsMatcher
    38. */
    39. @Bean("hashedCredentialsMatcher")
    40. public HashedCredentialsMatcher hashedCredentialsMatcher(){
    41. HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
    42. // 设置哈希算法名称
    43. matcher.setHashAlgorithmName("MD5");
    44. // 设置哈希迭代次数
    45. matcher.setHashIterations(1024);
    46. // 设置存储凭证(true:十六进制编码,false:base64)
    47. matcher.setStoredCredentialsHexEncoded(true);
    48. return matcher;
    49. }
    50. }

    写到这里Shiro的配置方面就写完了,接下来写用户登陆的controller类。

    8、登录controller

    1. @PostMapping(value = "/login")
    2. public Result login(@RequestBody UserVo userVo){
    3. //System.out.println(userVo);
    4. Subject subject = SecurityUtils.getSubject();
    5. UsernamePasswordToken token = new UsernamePasswordToken(userVo.getUserID(), userVo.getPassword());
    6. try {
    7. subject.login(token);
    8. // 加一些东西放在token里面(可以自定义)
    9. String role = ...你获取role的service...;
    10. String JwtToken = JwtUtil.createToken(userVo.getUserID(), role);
    11. // 返回给前端的DTO
    12. UserDTO userDTO = new UserDTO(userVo.getUserID(),role,JwtToken);
    13. // 返回给前端的Result
    14. return Result(Code.SUCCESS, userDTO, "Success");
    15. }catch (UnknownAccountException e) {
    16. // 提示账户错误
    17. } catch (IncorrectCredentialsException e) {
    18. // 提示密码错误
    19. }
    20. }

     五、前端代码实现

    界面代码,使用Vue3.0+Element Plus实现(简写,无style)

    login方法

    1. login(){
    2. //console.log(this.form)
    3. axios.post("你的路径", this.form).then((res)=>{
    4. //console.log(res)
    5. if(res.data.code === 20001){
    6. // 可以将一些数据保存到本地localStorage
    7. localStorage.setItem("token",res.data.data.token);
    8. ElMessage({
    9. message: '登录成功!',
    10. type: 'success',
    11. })
    12. router.replace("登陆成功之后跳转的界面");
    13. }else {
    14. ElMessage({
    15. message: res.data.msg,
    16. type: 'error',
    17. })
    18. }
    19. })
    20. }

    至此就登录功能就全部实现了。

    六、总结

    1、技术栈:

            SpringBoot+JWT+MyBatis Plus+Shiro+Vue+Element Plus

    2、身份验证步骤

    1. 收集用户身份 / 凭证,即如用户名 / 密码;

    2. 调用 Subject.login 进行登录,如果失败将得到相应的 AuthenticationException 异常,根据异常提示用户错误信息;否则登录成功;

    3. 最后调用 Subject.logout 进行退出操作
  • 相关阅读:
    JUC_8锁问题
    【混合编程】C/C++调用Fortran的DLL
    Windows Server 2012 R2系统服务器远程桌面服务多用户登录配置分享
    WebService: SpringBoot集成WebService实践一
    电脑技巧:Win10粘贴文件到C盘提示没有权限的解决方法
    企业中WEB前端项目开发流程
    密码学学习
    【c++刷题Day2】专题3栈与队列&单调栈与单调队列
    java---DFS算法---排列数字(每日一道算法2022.8.19)
    H3CNE-构建中小企业网络全套培训PPT汇总【V7版本】
  • 原文地址:https://blog.csdn.net/Hao_ge_666/article/details/126680167