一、准备工作
1.1新建用户表
DROP TABLE IF EXISTS `user_info`;
CREATE TABLE `user_info` (
`id` int(64) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`password` varchar(64) DEFAULT NULL,
`salt` varchar(64) DEFAULT NULL,
`state` varchar(8) DEFAULT NULL,
`username` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
插入一条数据: 这里使用Md5加密 加盐值,后文会将该工具类代码贴出
INSERT INTO `user_info` VALUES ('1', '管理员', '565111f370e4dd497aa4eeb985efa6a3', '7mrNoL/keN6rbqZDx+PKyg==', '1', 'admin');
1.2整体结构
1.3pom.xml
<properties>
<java.version>1.8</java.version>
<shiro.version>1.4.2</shiro.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
<!--SpringBoot热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional> <!-- 这个需要为 true 热部署才有效 -->
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- spring-boot-starter-thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<!--Shiro核心框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- Shiro使用Srping框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
1.4 application.properties 记得替换为自己的
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/数据库名?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username = 账号
spring.datasource.password = 密码
mybatis.mapper-locations=classpath:/mappers/*.xml
1.5 User类
public class User {
private Integer id;
private String name;
private String password;
private String salt;
private String state;
private String username;
geetter... setter...
toString...
1.6 UserService与UserServiceImpl
public interface UserService {
User getByUserName(String username);
}
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Override
public User getByUserName(String username) {
return userMapper.getByUserName(username);
}
}
1.7 UserMapper 与UserMapper.xml
@Mapper
public interface UserMapper {
User getByUserName(String username);
}
<?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.example.demo3.mapper.UserMapper">
<sql id="Base_Column_List">
id,name,password,salt,state,username
</sql>
<select id="getByUserName" parameterType="java.lang.String" resultType="com.example.demo3.domain.User">
select
<include refid="Base_Column_List"/>
from user_info where username = #{username}
</select>
</mapper>
1.8 LoginController
@Controller
public class LoginController {
private static Logger logger = LoggerFactory.getLogger(LoginController.class);
@RequestMapping("/login")
public String toLogin(){
return "login";
}
@RequestMapping("/doLogin")
public String doLogin(String username,String password,Model model){
//添加用户认证信息
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
username,
password);
//进行验证,这里可以捕获异常,然后返回对应信息
//默认登陆成功后跳转的是刚才访问的页面
try {
subject.login(usernamePasswordToken);
if (subject.isAuthenticated()){
return "redirect:/index";
}else{
usernamePasswordToken.clear();
return "redirect:/login";
}
}catch (UnknownAccountException e){
model.addAttribute("msg","用户名不存在");
return "login";
}catch (IncorrectCredentialsException e){
model.addAttribute("msg","密码错误");
return "login";
}
}
@RequestMapping("/index")
public String toIndex(Model model){
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();
model.addAttribute("user",user);
return "index";
}
}
1.9 相关页面
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>登录页</title>
<style>
.error{color: red}
</style>
</head>
<body>
<h1>Shiro登录页</h1>
<form id="loginForm" action="/doLogin" method="post">
<p th:text="${msg}" class="error"></p>
<input type="text" name="username" placeholder="输入用户名"/>
<input type="password" name="password" placeholder="输入密码"/>
<button type="submit">登录</button>
</form>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h1>欢迎!<p th:text="${user.name}"></p></h1>
<a href="/logout">安全退出</a>
</body>
</html>
二、Shiro相关配置
2.1 自定义域 CustomRealm
执行授权部分其实是要需要查询登录用户的角色与菜单权限的,本文只实现登录登出,则省略
public class CustomRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(CustomRealm.class);
@Autowired
UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
logger.debug("######## Shiro执行授权 ########");
//能进入这里表示账号已经通过验证了
User user = (User) principalCollection.getPrimaryPrincipal();
if (null != user){
//获取用户的角色与权限
//Set<String> roleNames = roleService.listRoleNames(user.getUserid());
// Set<String> perNames = permissionService.listPermissionNames(user.getUserid());
//授权对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//把角色与权限放进去
//info.setRoles(roleNames);
//info.setStringPermissions(perNames);
return info;
}
logger.debug("****授权失败****用户信息为空!");
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
logger.debug("######## Shiro执行认证 ########");
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//获取用户输入的账号
String username = upToken.getUsername();
System.out.println("####"+username);
//获取数据库中的密码
User user = userService.getByUserName(username);
user = userService.getByUserName(username);
if (null == user){
return null;
}
String passwordInDB = user.getPassword();
String salt = user.getSalt();
//把当前用户的认证信息存入SimpleAuthenticationInfo 并返回
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,passwordInDB,
ByteSource.Util.bytes(salt),getName());
return info;
}
}
2.2 Shiro配置类 ShiroConfiguration
@Configuration
public class ShiroConfiguration {
@Bean(name = "customRealm")
public CustomRealm myShiroRealm(HashedCredentialsMatcher matcher){
CustomRealm myShiroRealm= new CustomRealm();
//设置密码加密
myShiroRealm.setCredentialsMatcher(matcher);
return myShiroRealm;
}
//把realm注册到securityManager中
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager(HashedCredentialsMatcher matcher){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm(matcher));
return securityManager;
}
//Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String,String> map = new HashMap<String, String>();
//把处理登录的路径放行
map.put("/doLogin","anon");
//登出
map.put("/logout","logout");
//对所有用户认证
map.put("/**","authc");
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//加入注解的使用,不加入这个注解不生效
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
}
三、效果展示
登录页输入账号:admin 密码:123 点击登录
四、安全退出
4.1 LoginController中新增
@RequestMapping("/logout")
public String doLogOut(Model model){
Subject subject = SecurityUtils.getSubject();
subject.logout();
model.addAttribute("msg", "安全退出!");
return "login";
}
4.2 效果展示
五、扩展
- Shiro配置类中的 setSuccessUrl 认证成功后跳转路径,但是系统默认登录成功后首次跳转的地址为访问系统时初次使用地址, 比如说我们首次访问的是登录页面,那这里登录成功后它还是会跳转到登录页面。 解决这个的方法有好几种,我这里是在subject.login之后,根据subject.isAuthenticated()验证当前用户是否登录来进行重定向
- Shiro的退出登录其实是不需要在Controller中写的,只要拦截到访问
/logout
的请求,就会被走logout
对应的LogoutFilter
,自动登出,并且也会清理用户的session
信息 - logout 无参,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url
加密工具:
public class PasswordEncryptionUtil {
public static void main(String[] args) {
String password = "123";
String salt = new SecureRandomNumberGenerator().nextBytes().toString();
//String salt = "3SdRKRjauah+nwPf7g/VFQ==";
int times = 2;
String algorithmName = "md5";
String encodedPassword = new SimpleHash(algorithmName,password,salt,times).toString();
System.out.printf("原始密码是 %s , 盐是: %s, 运算次数是: %d, 运算出来的密文是:%s ",password,salt,times,encodedPassword);
}
public static Map<String, String> doEncrypt(String password,String salt){
Map<String, String> resultMap = new HashMap<>();
if (StringUtils.isBlank(salt)){
salt = new SecureRandomNumberGenerator().nextBytes().toString(); //盐量随机
}
//两次Md5加密
String encodedPassword= new SimpleHash("md5",password,salt,2).toString();
resultMap.put("salt",salt);
resultMap.put("encodedPassword",encodedPassword);
return resultMap;
}
}
demo已上传码云:https://gitee.com/wanglonewalker/demo3.git